How I can deselect button in angular? - javascript

I have some buttons like a category. When I click on one of them I get the card with some information.
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='{categoryId: category.id, page: 1}'
queryParamsHandling='merge'>
</app-category-button>
app-category-button selector:
<button mat-stroked-button
class='category-item font-medium'
[ngClass]='{"bg-primary-500 text-white": isSelected,
"text-primary-500 bg-lightbluebg-500": !isSelected}'>
{{label}}
</button>
When I click on the button I get in my route the queryParams that open my cards with information.
The question is how on the second click to that button hide queryParams and deselect the button?

You can do something like
this.route.queryParams.subscribe(
(params: Params) => {
this.queryParams = params.hasOwnProperty('categoryId') ?
{} : {categoryId: category.id, page: 1}
}
);
And use queryParams in your template as
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='queryParams'
queryParamsHandling='merge'>
</app-category-button>

If you want to remove the category only on the button for the selected one, you can change the categoryId query param to null only on click on that button.
<app-category-button [label]='category.name'
[isSelected]='category.id === (selectedCategoryId$ | async)'
[routerLink]='["."]' [queryParams]='{categoryId: (selectedCategoryId$ | async) ? null : category.id, page: 1}'
queryParamsHandling='merge'>
</app-category-button>
For the rest categories, it would still change load the respective category.

Accessing the router state
To retrieve the current variables from a route or current query params, use the ActivatedRoute-service.
In your routing, e.g. AppRouting:
import RouterModule.forRoot([
{ path: 'test/:id', component: MyComponent }
])
in your component:
#Component({ ... })
export class MyComponent implements OnInit, OnDestroy {
private readonly destroy$ = new Subject<void>();
constructor(readonly activatedRoute: ActivatedRoute) { }
ngOnInit(): void {
// snapshot gets the params just once
this.doSomethingWithRouteParams(
this.activatedRoute.snapshot.params,
this.activatedRoute.snapshot.queryParams);
// you can also listen to the changes using RxJS observables
combineLatest([
this.activatedRoute.paramMap,
this.activatedRoute.queryParamMap
])
.pipe(takeUntil(this.destroy$))
.subscribe(([params, queryParams]) =>
this.doSomethingWithRouteParams(params, queryParams));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private doSomethingWithRouteParams(params: ParamMap, queryParams: ParamMap): void {
console.log({
id: params.get('id'), // use the same param-name as in the router-config
categoryId: queryParams.get('categoryId'),
page: queryParams.get('page'),
});
}
}
See https://angular.io/guide/router-reference#activated-route for a list of properties on the activated route.
Conditional class binding
I assume by "deselecting the button" you probably mean conditionally binding the CSS classes. There are several ways to achieve this in Angular, the one I prefer over others due to readability would look like so:
<button
mat-stroked-button
class="category-item font-medium"
[class.text-white]="isSelected"
[class.bg-primary-500]="isSelected"
[class.text-primary-500]="!isSelected"
[class.bg-lightbluebg-500]="!isSelected"
>
{{ label }}
</button>
See the guide on binding to attributes and to the class attribute in particular.
Hint regarding the async pipe
Also, just as a side note: Note that [isSelected]='category.id === (selectedCategoryId$ | async)' will create a new subscription for every single app-category-button instance in this template. This might cause poor performance so you probably should refactor this to get the selected category just once, e.g.:
<ng-container *ngIf=(selectedCategoryId$ | async) as selectedCategoryId>
<app-category-button
label="Category #1"
[isSelected]="1 === selectedCategoryId"
></app-category-button>
<app-category-button
label="Category #2"
[isSelected]="2 === selectedCategoryId"
></app-category-button>
<app-category-button
label="Category #3"
[isSelected]="3 === selectedCategoryId"
></app-category-button>
</ng-container>
Please refer to the "Consuming observable data with ngIf and the async pipe"-section in the Angular ngIf: Complete Guide

Related

Angular 15, child component doesn't emit string to parent

I have this child component (filters.component.ts) from which I am trying to emit a string to the parent component. I had already done this once with a different component but it seems like Angular doesn't like me implementing an *ngFor to loop through a string array and pass the category string to the method? I've tried adding a console log to the onShowCategory() method in home.component.ts and it does not log any string values to the console, leading me to believe that the values are not being passed to the parent when the click event is activated. Here is the code (I've added arrows to point to the relevant lines of code, they are not part of my code and not the issue.):
filters.component.html:
<mat-expansion-panel *ngIf="categories">
<mat-expansion-panel-header>
<mat-panel-title>CATEGORIES</mat-panel-title>
</mat-expansion-panel-header>
<mat-selection-list [multiple]="false">
<mat-list-option *ngFor="let category of categories" [value]="category"> <--------
<button (click)="onShowCategory(category)">{{ category }}</button> <--------
</mat-list-option>
</mat-selection-list>
</mat-expansion-panel>
filters.component.ts:
#Component({
selector: 'app-filters',
templateUrl: './filters.component.html',
styleUrls: []
})
export class FiltersComponent {
#Output() showCategory = new EventEmitter<string>() <-------
categories: string[] = ['shoes', 'sports']; <-------
onShowCategory(category: string): void { <-------
this.showCategory.emit(category); <-------
}
}
home.component.html:
<mat-drawer-container [autosize]="true" class="min-h-full max-w-7xl mx-auto border-x">
<mat-drawer mode="side" class="p-6" opened>
<app-filters (showCategory)="onShowCategory($event)"></app-filters> <-------
</mat-drawer>
<mat-drawer-content class="p-6">
<app-products-header (columnsCountChange)="onColumnsCountChange($event)"></app-products-header>
{{ category }} <----- should display the category when selected
</mat-drawer-content>
</mat-drawer-container>
home.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: []
})
export class HomeComponent {
cols = 3;
category: string | undefined; <-------
onColumnsCountChange(colsNumber: number): void {
this.cols = colsNumber
}
onShowCategory(newCategory: string): void { <-------
this.category = newCategory; <-------
}
}
I have read through and followed the variables many times and I don't see the issue. From the child component template I pass the category to the onShowCategory method and emit it to the parent. From the parent I call the EventEmitter and pass the $event variable which should change the value of the category property in the home component. I've checked spelling, and tried moving the tags around. I don't see a console.log in the console when I add one to the method, and I cannot get the string to appear on the home template. What am I doing wrong?
your parent html should have messageEvent. Give this a try and see if it works.
<app-filters (messageEvent)="onShowCategory($event)"></app-filters>
<app-products-header (messageEvent)="onColumnsCountChange($event)"></app-products-header>
I also like to put console.log statements at every point when setting up EventEmitters, just to see where it's getting stuck.
After taking a break and spending a few more hours combing the internet for answers (to no avail) I decided to try something completely unintuitive and solved it.
I changed:
<mat-list-option *ngFor="let category of categories" [value]="category">
<button type="button" (click)="onShowCategory(category)">{{ category }}</button>
</mat-list-option>
To
<mat-list-option *ngFor="let category of categories" (click)="onShowCategory(category)" [value]="category">
<div>{{ category }}</div>
</mat-list-option>
and now it works, not entirely sure why the click event needs to be on the parent tag and cannot be on the button itself. The thought process that got me to this solution was that the click event seemed to not be activating and/or the strings in the categories array weren't being passed to the method properly. I hope this helps someone in the future.

Angular - Using custom directive to load several components is returning undefined

I am developing an app and for now, I have a dynamic grid generator which divides the space in the screen to fit several components dynamically. So, the component encharged of this must render the components after angular has rendered the page. In order to achieve that I've followed the angular dynamic component loader guide (https://angular.io/guide/dynamic-component-loader).
So I am in a point where I do have the component where the other components must be rendered, I have my custom directive to render the components.
The directive
#Directive({
selector: '[componentLoader]'
})
export class ComponentLoaderDirective {
constructor (
public ViewContainerRef: ViewContainerRef
) {}
}
Now the component ( grid component )
grid.component.ts
// ... Stuff above
export class GridComponent implements OnInit {
#Input() public items: gridItem[] = [];
#ViewChild(ComponentLoaderDirective) componentLoader: ComponentLoaderDirective | undefined;
constructor(
private sanitizer: DomSanitizer,
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit(): void { this.processRow(this.items) }
processRow( row: gridItem[] ) {
// Some grid related stuff ...
for ( let item of row ) {
// Stuff performed over every item in each grid row
this.renderComponentIfNeeded(item)
}
}
renderComponentIfNeeded( item: gridItem ):void {
if ( item.components ) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
let viewContainerRef = this.componentLoader.ViewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
componentRef.instance.data = item;
console.log('Directive ', this.componentLoader, 'ComponentRef: ', componentRef);
}
}
And the HTML of the component:
<!-- Dynamic grid generation using ng-template and ng-content. This is generated several times using the *ngFor, for every item in the items array we will have a componentLoader -->
<ng-template componentLoader>
</ng-template>
There is a lot more content in these files but for simplicity I will only post this, If you need more code just tell me.
Okay, so my problem is that when I access to this.contentLoader the returned value is just undefined, so this.componentLoader.viewContainerRef causes an error because componentLoader is undefined.
I've tried adding the exportAs property to the directive's decorator and it is giving exacly the same error.
I've also tried to add the directive in the module declarations without success, and changed the <ng-template componentLoader> to <ng-template #loader=componentLoader> which causes a different error ( No directive has 'componentLoader' exportAs or something like this )
PS: In the ´´´this.componentFacotryResolver.resolveComponentFactory(component)``` I successfully have each component that has been given to the grid.
I prefer you not to solve my issue but to point me in the right direction and help me see what am I doing wrong in order to improve myself.
Any help will be much appreciated :)
I've managed to solve this issue in a very simple way.
I was trying to do too many things inside the grid component so I removed to code related to the component loader and moved it into a single component, called ComponentLoaderComponent.
Inside the component I've setted up all the logic in the same way than I did in the grid component. So now I have a new ts file like this:
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '#angular/core';
import { ComponentLoaderDirective } from 'src/app/shared/directives/componentLoader.directive';
#Component({
selector: 'component-loader',
templateUrl: './component-loader.component.html',
styleUrls: ['./component-loader.component.css']
})
export class ComponentLoaderComponent implements OnInit {
#Input() public component: any;
#ViewChild(ComponentLoaderDirective, { static: true }) componentLoader!: ComponentLoaderDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit(): void {
this.loadComponent();
}
loadComponent():void {
if (this.component) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
let viewContainerRef = this.componentLoader.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
}
}
And an HTML like this:
<ng-template componentLoader>
</ng-template>
Now from the grid component I only have to call the ComponentLoader for every component I want to add to the grid, so the grid html will look like this:
<div
*ngIf=" gridItem.components && gridItem.components.length > 0"
class="component-container"
>
<component-loader
*ngFor="let component of gridItem.components"
[component]="component">
</component-loader>
</div >
Now the components are getting loaded correclty, anyways I still don't know what I was missing in before.

How to receive value of 'n' input boxes?

How to receive the input values of variable number of input boxes?
I searched a long time in Google but I'm not able to do it.
ParentComponent.html:
<my-Input [interfaces]="interfaces"> {{ title }}</my-Input>
ParentComponent.ts:
interface single_input{
title: string,
get_value(event: string): void;
}
interfaces: single_input[] = [
{
title: "something",
get_value(event){ console.log(event)}
}
];
ChildComponent.html:
<div *ngFor="let i of interfaces">
<Input (keyup)="i.get_value($event.target.value)"> {{ title }}</Input>
</div>
To log this in the console works, but I just want to bind the input values to the given interface and give it back by eventemitter to the parent.
How to do this? Do I need a class for it?
I got it now.
But it looks like a workaround. is there a better solution than following code?
Code-Update:
ParentComponent.ts:
interface single_input{
title: string;
value: string;
}
interfaces: single_input[] = [
{
title: "something",
}
];
ChildComponent.html:
<div *ngFor="let i of interfaces">
<Input (keyup)="i.value = $event.target.value"> {{ title }}</Input>
</div>
It is far simpler than what you are trying to do. Angular have specific mechanism to achieve this goal, one of them being the decorators #Input and #Output. You are already using the #Input decorator to pass in the data to the child component, now you just need to use the #Output decorator to pass the data back to the parent. I recommend looking at the docs and this example to have a better understanding on how this works. Below is an example for your use case.
ChildComponent.html
<div *ngFor="let i of interfaces">
<input (keyup)="onKeyUp(i, $event.target.value)"> {{ title }}</input>
</div>
ChildComponent.ts
import { Component, EventEmitter } from '#angular/core';
#Component({ ... })
export class ChildComponent {
...
#Input() interfaces;
#Output() childValue: EventEmitter<any> = new EventEmitter<any>();
// ^ Use a proper datatype ^
onKeyUp(source, value) {
this.childValue.emit({
source: source,
value: value
});
}
...
}
ParentComponent.html
<my-input [interfaces]="interfaces" (childValue)="onChildValueChange($event)"> {{ title }}</my-input>
ParentComponent.ts
import { Component} from '#angular/core';
#Component({ ... })
export class ParentComponent {
...
onChildValueChange(event) {
// Do what you want with it
const interface = this.interfaces.find(interface =>
interface.title === event.source.title);
console.log('[ParentComponent] Value from child component:', event);
console.log('[ParentComponent] Changed interface:', interface);
console.log('[ParentComponent] Changed value:', event.value);
}
...
}
Extra tip: Just for completeness, an equally popular approach, would be using a service to contain the data, where the child changes the data on the service and the parent read from it.

Angular 2 - Manipulating rxjs Observable in a view

I'm getting started with Observable in Angular 2 and I can't figure out how to use them properly in my views.
I'm using Angular 2 with angular-redux, and using the #select() decorator to retrieve my selectedMovie$ from the redux store. This part works fine, the component basically dispatch a redux event to set the default selectedMovie$ upon init. The redux store is correctly updated, but when I try to consumme it in the view, I face some issues.
import { Component, OnInit } from '#angular/core';
import { NgRedux, select } from '#angular-redux/store';
import { MovieActions} from '../store/app.actions';
import { IAppState} from '../store/reducers';
import { MovieService } from '../movie.service';
import { Observable } from 'rxjs/Observable';
import { IMovie } from '../movie.model';
#Component({
selector: 'app-movies-container',
templateUrl: './movies-container.component.html',
styleUrls: ['./movies-container.component.css'],
providers: [MovieService]
})
export class MoviesContainerComponent implements OnInit {
movies: Array<IMovie>;
#select() readonly selectedMovie$: Observable<IMovie>; // HERE - Get data from the redux store
constructor(
private movieService: MovieService,
private movieActions: MovieActions,
private ngRedux: NgRedux<IAppState>
) { }
ngOnInit() {
// Fetch movies and use the first one as default displayed movie.
this.getMovies() // HERE - Takes the first movie and make it the default selectedMovie by dispatching a redux action
.then(movies =>
this.ngRedux.dispatch(this.movieActions.changeSelectedMovie(this.movies[0]))
);
}
getMovies() { // HERE: Call an API, returns an array of movies in data.results
return this.movieService.getMostPopular()
.then(data => this.movies = data.results);
}
onSelect(movie: IMovie) {
this.ngRedux.dispatch(this.movieActions.changeSelectedMovie(movie));
}
}
Here comes the view:
<div *ngIf="movies">
<md-list>
<h3 md-subheader>Most popular movies NOW!</h3>
<pre>
{{(selectedMovie$ | async | json).id}} // This fails and displays nothing. I'd expect it to display the movie id
</pre>
<md-list-item
*ngFor="let movie of movies"
[class.selected]="movie.id === (selectedMovie$ | async | json).id" // This is a real deal, I don't know what's the syntax to use. I wanted to compare ids
(click)="onSelect(movie)"
>
<img src="https://image.tmdb.org/t/p/w92{{movie.poster_path}}" />
{{movie.title}}
</md-list-item>
</md-list>
<app-movie-card [movie]="selectedMovie$ | async | json"></app-movie-card> // This component gets the object correctly formatted
</div>
Maybe I'm just not using the right syntax. Or maybe I shouldn't use an Observer in the view in the first place?
Edit: Solution
<div *ngIf="movies && (selectedMovie$ | async); let selectedMovie">
<md-list>
<h3 md-subheader>Most popular movies NOW!</h3>
<md-list-item
*ngFor="let movie of movies"
[class.selected]="movie.id === selectedMovie.id"
(click)="onSelect(movie)"
>
<img src="https://image.tmdb.org/t/p/w92{{movie.poster_path}}" />
{{movie.title}}
</md-list-item>
</md-list>
<app-movie-card [movie]="selectedMovie"></app-movie-card>
</div>
The problem results from a common misconception that JSON is a synonym for plain object. It isn't.
json pipe converts input into actual JSON string. So (selectedMovie$ | async | json) expression evaluates to a string and doesn't have id property.
It is helpful to use AoT compilation, because it allows to detect type problems in template and would likely result in type error in this case.
It should be (selectedMovie$ | async).id instead.
If (selectedMovie$ | async) is used more than once (like in this case), it will result in several subscriptions. It can be optimized by assigning it to local variable, as explained here:
<div *ngIf="movies">
<ng-container *ngIf="(selectedMovie$ | async); let selectedMovie">
...
{{selectedMovie.id}}
...
</ng-container>
</div>

how to create a component that i can set fetching url data in an attribute in angular 2

I'm trying to create a application with angular 2 , i want to create a component in angular 2 that I set URL in attribute and want use several times from this component and each component have own data...
i want something like this :
its possible or not?
I'll really appreciate if someone help me.
new movies :
<comp url="www.aaaa.com/movies?type=new"></comp>
old movies :
<comp url="www.aaaa.com/movies?type=old"></comp>
#Component({
selector: 'comp',
template: '<div>{{data}}</div>'
})
export class Component {
#Input() url: string;
constructor(private http:Http) {
}
ngOnChanges(changes) {
this.http.get(this.url)
.map(res => res.json())
.subscribe(val => this.data = val);
}
}
If the component has more than one input then you need to check which one was updated. See https://angular.io/api/core/OnChanges for more details.
You can use compoenent as mentioned above answer.
But for this kind of task I always choose directive. You can also create a directive which will take one parameter and do the stuff.
In this way you don't have to create a tag but in any tag you can apply your directive.
#Directive({
selector: '[comp]',
})
export class compDorective implements OnInit {
#Input() url: string;
constructor(private http:Http, private elementRef: ElementRef) {
}
ngOnInit(changes) {
this.http.get(this.url)
.map(res => res.json())
.subscribe(val => this.elementRef.nativeElement.innerHtml = val);
}
}
you can apply this directive to any element like this
<div comp [url]="www.aaaa.com/movies?type=new"></div>
<span comp [url]="www.aaaa.com/movies?type=old"></span>

Categories