Unwanted component method execution during a mouse click event - html

I'm currently working on a component that displays a list of items using material grid list and material cards, where an item will be displayed only if it is exists in a given datasource. So far I am getting the result I need, but upon further inspection, I tried to log the method that I am calling to check if the item exists into the console and that's where I discovered that anytime I click on the page during testing/debugging, the method gets executed. I am just worried if this will somehow affect the performance of the app.
I haven't specifically tried anything yet as I am still unaware how this is happening (I am a beginner to angular, please bear with me)
HTML
<mat-grid-list cols="4" rowHeight=".85:1">
<div *ngFor="let item of items">
<mat-grid-tile *ngIf="item.isActive">
<mat-card class="mat-elevation-z10 item-card">
<mat-card-header>
<mat-card-title>{{item.title}}</mat-card-title>
<mat-card-subtitle>{{item.subtitle}}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="{{item.icon}}" alt="{{item.name}}">
<mat-card-content>{{item.description}}</mat-card-content>
<mat-divider [inset]="true"></mat-divider>
<mat-card-actions>
<button mat-button
[disabled]="!isAccessible(item.name)">Action1</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
</div>
</mat-grid-list>
COMPONENT
export class ItemComponent implements OnInit {
items: any;
dataSource: ItemDataSource; //items from the back end server
constructor(private store: Store<AppState>) { }
ngOnInit() {
this.items = fromConfig.ITEMS;
this.dataSource = new ItemDataSource(this.store);
this.dataSource.load();
}
isAccessible(itemName: string) {
return this.dataSource.isAccessible(itemName);
}
}
DATASOURCE
export class ItemDataSource implements DataSource<Item> {
itemSubject = new BehaviorSubject<Item[]>([]);
constructor(private store: Store<AppState>) { }
isAccessible(itemName: string): boolean {
let exists = false;
for (const itemSubject of this.itemSubject.value) {
console.log('Parameter Item Name: ' + itemName + '; Subject Item Name: ' + itemSubject.name);
if (itemSubject.name === itemName ) {
exists = true;
break;
}
}
return exists;
}
connect(collectionViewer: CollectionViewer): Observable<Item[]> {
return this.itemSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.itemSubject.complete();
}
}
Expected result would be that the method will be executed only once during initialization or after refresh.

You are using square brackets bind the disable property of the button. This binds the function with that button state. So, the function is called every time the page is being rendered. To use the function only once (as you intended), remove the braces.
<button mat-button disabled="!isAccessible(item.name)">Action1</button>
This will call the function only once when the page is rendered intially.

Related

Angular: is it possible to bundle attribute bindings in a template, similar as ng-templates for HTML-elements?

Is it possible to combine multiple attribute-bindings within the HTML of an Angular component to prevent duplicated code?
For example, we currently have a component which has a button and an anchor like this:
<ng-template #button>
<button [class]="classes"
(click)="onClick($event)"
[attr.disabled]="disabled">
<ng-template [ngTemplateOutlet]="buttonContent"></ng-template>
</button>
</ng-template>
<ng-template #anchor>
<a [class]="classes"
(click)="onClick($event)"
[attr.disabled]="disabled"
[href]="href">
<ng-template [ngTemplateOutlet]="buttonContent"></ng-template>
</a>
</ng-template>
<ng-template #anchorWithoutHref>
<a [class]="classes"
(click)="onClick($event)"
[attr.disabled]="disabled">
<ng-template [ngTemplateOutlet]="buttonContent"></ng-template>
</a>
</ng-template>
As you can see, we have [class]="classes" (click)="onClick($event)" [attr.disabled]="disabled" on all three variants. Is it possible to create some kind of template or anything that's re-usable, so these attribute-bindings are only done on one place?
(We use Angular version 15.1.3.)
EDIT: classes; disabled; and onClick(...) are values/getters in my button.component.ts (and disabled is an #Input as well):
get classes(): string {
let classesBuilder = new ClassesBuilder();
...
return classesBuilder.toString();
}
private _disabled: boolean = false;
#Input()
set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);
}
get disabled(): boolean {
return this._disabled;
}
onClick($event: MouseEvent) {
if (this.href && (!this.useButtonClick || $event.ctrlKey || $event.shiftKey)) {
return; // Default behaviour
}
$event.preventDefault();
$event.stopPropagation();
if (!this.disabled) {
this.buttonClick.emit();
}
}
This disabled is used in a few places within if-checks (like the onClick above, but also some other functions).
So the Directive approach suggested by #WouterSpaak does sound promising, and would indeed work if I could have moved the entire functionality/values to that directive. But if I want to pass these values above from my button.component.ts to the Directive, I think I still need duplicated attributes to pass that data to the Directive, making it redundant in my case..
Create a directive:
#Directive({
selector: 'sharedAttrs'
})
export class SharedAttrsDirective {
#HostBinding('class')
protected readonly clz = classes;
#HostBinding('attr.disabled')
protected isDisabled() {
return someBoolean;
}
#HostListener('click', ['$event'])
protected handleClick(event: MouseEvent) {
doStuffWith(event);
}
}
Then in your template, you can do something like:
<button sharedAttrs></button>
<a sharedAttrs href="www.stackoverflow.com"></a>
<some-other-element-or-component sharedAttrs></some-other-element-or-component>

Not mandatory option selection in Autocomplete Angular Material [duplicate]

I'm trying to implement the autocomplete component from Angular Material:
https://material.angular.io/components/autocomplete/overview
It works well for letting the user select a particular item from the suggested list but I also want to allow the user to add items not in the list.
So lets say the suggested list has the following items:
Cats
Birds
Dogs
And the user starts typing "Do" and the autocomplete shows "Dogs" as the suggested option (because I'm also filtering the list based on what they type). But then the user continues typing "Dolls" and now nothing is displayed in the autocomplete suggestions. Then the user hits enter and it gets added to the list.
Current behavior is that if what the user typed doesn't exist in the list then they are unable to add the item.
If you add an enter key listener to the input field, you can process the entered value and add it to the options if it doesn't exist. You can also dynamically add whatever the user enters to the list of filtered options as an "add new item" option, or add an "add" icon to the field (e.g. as a matSuffix). Or you can do all three:
Stackblitz
HTML
<form class="example-form">
<mat-form-field class="example-full-width">
<input matInput placeholder="Item" aria-label="Item" [matAutocomplete]="auto" [formControl]="itemCtrl" (keyup.enter)="addOption()">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected($event.option)">
<mat-option *ngFor="let item of filteredItems | async" [value]="item">
<span>{{ item }}</span>
</mat-option>
</mat-autocomplete>
<button *ngIf="showAddButton && itemCtrl.value" matSuffix mat-button mat-icon-button (click)="addOption()"><mat-icon matTooltip='Add "{{itemCtrl.value}}"'>add</mat-icon></button>
</mat-form-field>
</form>
TS
import { Component } from '#angular/core';
import { FormControl } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { startWith } from 'rxjs/operators/startWith';
import { map } from 'rxjs/operators/map';
/**
* #title Autocomplete with add new item option
*/
#Component({
selector: 'autocomplete-overview-example',
templateUrl: 'autocomplete-overview-example.html',
styleUrls: ['autocomplete-overview-example.css']
})
export class AutocompleteOverviewExample {
itemCtrl: FormControl;
filteredItems: Observable<any[]>;
showAddButton: boolean = false;
prompt = 'Press <enter> to add "';
items: string[] = [
'Cats',
'Birds',
'Dogs'
];
constructor() {
this.itemCtrl = new FormControl();
this.filteredItems = this.itemCtrl.valueChanges
.pipe(
startWith(''),
map(item => item ? this.filterItems(item) : this.items.slice())
);
}
filterItems(name: string) {
let results = this.items.filter(item =>
item.toLowerCase().indexOf(name.toLowerCase()) === 0);
this.showAddButton = results.length === 0;
if (this.showAddButton) {
results = [this.prompt + name + '"'];
}
return results;
}
optionSelected(option) {
if (option.value.indexOf(this.prompt) === 0) {
this.addOption();
}
}
addOption() {
let option = this.removePromptFromOption(this.itemCtrl.value);
if (!this.items.some(entry => entry === option)) {
const index = this.items.push(option) - 1;
this.itemCtrl.setValue(this.items[index]);
}
}
removePromptFromOption(option) {
if (option.startsWith(this.prompt)) {
option = option.substring(this.prompt.length, option.length -1);
}
return option;
}
}
It's weird that the user can add an item in the suggested list. The list is suggested to the user by someone who knows what to suggest. But anyway...
The user can type anything in the field and ignore the suggestions. By ignoring the suggested Dogs and typing Dolls, user can press an "Add" button which will add whatever is typed in (Dolls) to the options array.
For example, you can do it by listening to the submit event on the form:
(ngSubmit)="options.push(myControl.value); myControl.reset()"
Here's the complete demo as well.

Calling a method from another component to keep data in sync

Problem
I can not refresh a variable, and thus select dropdown in the club-list-component after I create a club in the create-club-component
Context:
I am developing an application which randomly select a person from a team, from a club. First I made 1 component which concluded all the functionality, but as that would be ugly I wanted to seperate the different components and functions.
What I tried:
I've tested the functionality and the dropdown-box refreshes after creating a club, if all code is contained in 1 component.
Code snippets
I have the following pieces of code to share (some left away for readability):
create-club.component.ts:
#Input() clubDetails = {name: ''};
createClub() {
this._clubService.createClub(this.clubDetails).subscribe((data: {}) => {
});
alert('Club Created');
}
club-list.component.ts:
public clubs = [];
ngOnInit() {
this.refreshClublist();
}
refreshClublist() {
this._clubService.getClubs().subscribe(data => this.clubs = data);
}
}
club-list.component.html
<div>
<div class="alert alert-primary">
Select a club from the list
</div>
<select class="form-control">
<option *ngFor="let club of clubs" [value]="club.id">
{{club.name}}
</option>
</select>
</div>
What do I try to archieve:
Once i create my club from the popup modal in create-club.component.html, I want to have the dropdown box in club-list.component.html to be refreshed
In my mind best case scenario would be:
[club-list-component] ngOnInit(refreshClublist()) {}
[create-club.component]createClub()
[club-list-component] refreshClublist() (called after createClub() in step 2)
You can achieve this using Observable BehaviorSubject
create-club.component.ts:
private clubList = new BehaviorSubject(null);
public clubList$ = this.clubList.asObservable();
createClub() {
this._clubService.createClub(this.clubDetails).subscribe((data: {}) => {
this.clubList.next(data);
});
alert('Club Created');
}
club-list-component:
ngOnInit(){
this.clubList$.subscribe(updatedList=>{
console.log(updatedList);
});
}

Angular material checkbox automatically un-checks itself

I have a list that I display as checkboxes using angular-material (Angular 7). Below I will add code snippet for .html and .ts files.
Whenever I click on a checkbox it is checked but then immediately un-checked. I entered in debug mode and see that when I click on a checkbox, my isSelected() method gets called 4 times by Angular. When I click on it, it immediately goes to checked state. Then it is still checked the second time that Angular calls it. On the third time, it becomes un-checked (meanwhile isSelected() is still true). I cannot figure out what I did wrong. What I tried is:
Switch from isSelected() method to a class property (added the isSelected boolean field on myListItem objects)
Added bidirectional binding on top of the previous idea
Switch from checked to ngModel
Nothing helped. What else to try, I don't know. Please help me out.
html snippet:
class MyListItem {
id: number
name: string
}
// omitted annotations
export class MyComponent implements OnInit, OnDestroy {
myList: MyListItem[] = [] // omitted initialization
isSelected(myListItem: MyListItem): boolean {
return this.myList.includes(myListItem)
}
toggle(myListItem: MyListItem): void {
// omitted the code, I debugged it and it works correctly:
// it adds/removes the item to/from the list
}
}
<mat-list>
<mat-list-item *ngFor="let myListItem of myList">
<mat-checkbox flex="100" (click)="toggle(myListItem)"
[checked]="isSelected(myListItem)">
{{ myListItem.name }}
</mat-checkbox>
</mat-list-item>
</mat-list>
Use change event not click:
<mat-checkbox flex="100" (change)="toggle(myListItem)"
[checked]="isSelected(myListItem)">
{{ myListItem.name }}
</mat-checkbox>
I am not sure if this will work but you can add an Event parameter to the toggle function.
toggle(myListItem: MyListItem, event: any) { event.preventDefault() }
Then in your html:
(click)="toggle(myListItem, $event)"
Again, Not sure if this will work, but I have found that sometimes these click events will happen automatically, unless the prevent default() function is called

Angular *ngFor doesnt update list on add/delete

I have an app where I have a list of vehicles. I have a local .json file where I get my data. This data is updated with a web-api. Whenever I add a vehicle to the list it is updated in the .json file, but I have to refresh the web browser to see the updated result. It works in the same way when I am trying to delete a vehicle from the list. I use one local list to get quick returns and then I use a second list to make sure that the changes are saved to the .json file. See code below.
Typescript
// component
vehicle: VehicleDetail;
favVehicles: VehicleDetail[] = [];
favVehiclesLocal: VehicleDetail[] = [];
ngOnInit() {
this.vehicleService.getFavourite().subscribe(data => {
this.favVehicles = data;
this.favVehiclesLocal = [...data];
});
}
}
// Button function which adds the selected vehicle to your favourites
addFav(event: VehicleDetail): VehicleDetail[] {
this.favVehiclesLocal = [this.vehicle, ...this.favVehiclesLocal];
console.log(this.favVehiclesLocal);
this.vehicleService.addVehicle(event).subscribe(data => {
event = data;
});
return this.favVehiclesLocal;
}
// Button function which deletes the selected vehicle from your favourites
deleteFav(event: VehicleDetail): VehicleDetail[] {
this.favVehiclesLocal = this.favVehiclesLocal.filter(h => h !== event);
this.vehicleService.deleteVehicle(event).subscribe(data => {
this.favVehicles = this.favVehicles.filter(h => h !== event);
event = data;
});
return this.favVehiclesLocal;
}
console.log(this.favVehiclesLocal);
}
The data is coming from a database and I use the following services to call for the data.
// Service for "add to favourite" button
addVehicle(vehicle: VehicleDetail): Observable<VehicleDetail> {
const url = `${this.API_URL}/favourites`;
const service = this.http
.post<VehicleDetail>(url, vehicle, this.httpOptions)
.pipe(
tap(_ => this.log(`adding vehicle id=${vehicle.id}`)),
catchError(this.handleError<VehicleDetail>('addVehicle'))
);
console.log(service);
return service;
}
// Service for "delete from favourite" button
deleteVehicle(vehicle: VehicleDetail): Observable<VehicleDetail> {
const url = `${this.API_URL}/favourites`;
const service = this.http
.put<VehicleDetail>(url, vehicle, this.httpOptions)
.pipe(
tap(_ => this.log(`deleted vehicle id=${vehicle.id}`)),
catchError(this.handleError<VehicleDetail>('deleteVehicle'))
);
console.log(service);
return service;
}
Html
<!-- list of vehicles -->
<aside *ngIf="favVehiclesLocal" class="vehiclelist">
<mat-nav-list matSort (matSortChange)="sortData($event)">
<th mat-sort-header="timestamp">Time of alarms</th>
<th mat-sort-header="status">Severity of status</th>
<mat-list-item *ngFor="let stuff of favVehiclesLocal" class="vehicles">
<span [ngClass]="getColors(stuff)"></span>
<p matLine (click)="updateInfo(stuff.id)"> {{ stuff.name }} </p>
<button mat-icon-button id="btn" *ngIf='check(stuff.alarm)' matTooltip="{{stuff.alarm[tooltipIndex(stuff)]?.timestamp}} - {{stuff.alarm[tooltipIndex(stuff)]?.description}}">
<mat-icon>info</mat-icon>
</button>
</mat-list-item>
</mat-nav-list>
</aside>
// add and delete buttons
<div class="details">
<button mat-raised-button #add (click)="addFav(vehicle)">Add to favourite</button>
<button mat-raised-button #delete (click)="deleteFav(vehicle)">Remove from favourite</button>
</div>
What is going wrong here? I have been checking out the Tour of Heroes on Angulario ( https://stackblitz.com/angular/akeyovpqapx?file=src%2Fapp%2Fheroes%2Fheroes.component.ts ) at .src/app/heroes/ and I havent been able to see a difference in their code and my code.
If you want me to clearify something or if you would like additional information please let me know.
Update
It should be mentioned that I have two views. These views are either the full list or the "my favourite" list. The lists are displayed depending on the value of a slide-toggle.
In my code above I wrote *ngFor="let stuff of favVehiclesLocal" to hide unknown parts of my code since I thought I had the problem narrowed down. The complete app uses a slightly different approach.
//app.component.html
<!-- list of vehicles -->
<aside *ngIf=".........
<mat-list-item *ngFor="let stuff of sortedVehicles" class="vehicles">
.........
</aside>
The sortedVehicles is assigned in the following way:
//app.component.html
<mat-slide-toggle (change)="myFavourite(favVehiclesLocal)">Show favourites</mat-slide-toggle>
// app.component.ts
myFavourite(vehicles: VehicleDetail[]): VehicleDetail[] {
this.toggleChecked = !this.toggleChecked;
console.log(this.toggleChecked);
if (this.toggleChecked) {
this.sortedVehicles = vehicles.slice();
} else {
this.sortedVehicles = this.vehicleDetails.slice();
}
console.log(this.sortedVehicles);
return this.sortedVehicles;
}
I start to think that this line of code start to complicate things? Is there any way that I can register the change? Is there any more effective approaches to it?