Im getting items from a cart service and putting them into my shopping cart component by making them public shoppingCartItems$ Observable<ISticker[]> = of ([]) and then subscribing that public shoppingCartItems: ISticker[] = [];
but when I place them into my html in an ngfor loop it tells me the properties are undefined
import { Component, OnInit } from '#angular/core';
import {CartService} from '../cart.service'
import { of } from 'rxjs';
import {Observable} from 'rxjs'
import { ISticker } from '../classes/isticker';
#Component({
selector: 'app-shopping-cart-component',
templateUrl: './shopping-cart-component.component.html',
styleUrls: ['./shopping-cart-component.component.css']
})
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]> = of([]);
public shoppingCartItems: ISticker[] = [];
constructor(private cartService: CartService) {
this.shoppingCartItems$ = this
.cartService
.getItems();
this.shoppingCartItems$.subscribe(_ => this.shoppingCartItems = _);
}
ngOnInit() {
}
In my HTMl I've tried using the async pipe line to unwrap the information
<p>You have {{shoppingCartItems.length}} items in your bag</p>
<ng-container *ngIf=" shoppingCartItems$ | async as shoppingCartStickers"></ng-container>
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
the output to the page shows <p>You have {{shoppingCartItems.length}} items in your bag</p> the amount of items in the cart but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag.
export class CartService {
private itemsInCartSubject: BehaviorSubject<ISticker[]> = new BehaviorSubject([]);
private itemsInCart: ISticker[] = [];
constructor() {
this.itemsInCartSubject.subscribe(_ => this.itemsInCart = _);
}
public addToCart(item: ISticker){
this.itemsInCartSubject.next([...this.itemsInCart, item]);
}
public getItems(): Observable<ISticker[]> {
return this.itemsInCartSubject.asObservable();
}
This is my cart.services.ts for reference. This is my first time posting to stack overflow so if there is anything that would be helpful to see please let me know as I'm happy to provide it.
Alternatively, you can have your ShoppingCartComponentComponent something like this:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartStickers: ISticker[] = [];
constructor(
private cartService: CartService
) { }
ngOnInit() {
this.cartService.getItems()
.subscribe(items => {
this. shoppingCartStickers = items;
});
}
}
Now, shoppingCartStickers can be directly used in the component html:
<p>You have {{shoppingCartStickers?.length}} items in your bag</p>
<ng-container *ngIf="shoppingCartStickers?.length">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
the output to the page shows You have {{shoppingCartItems.length}} items in your bag the amount of items in the cart
It's because you use shoppingCartItems which calculated in subscribe and will be up to date on every emitted event from shoppingCartItems$.
but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag
It's because the template variable shoppingCartStickers available only in the block you define it. So you just need to do:
<ng-container *ngIf="(shoppingCartItems$ | async) as shoppingCartStickers">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
I recommended you to use single variable for that. You don't need to do shoppingCartItems$ | async as shoppingCartStickers in template because this data has already stored in the shoppingCartItems property of the component.
Your issue is that you have misplaced the ending tag of <ng-container>.
What you have is essentially:
<ng-container>
<!-- Stickers available ONLY here -->
</ng-container>
<!-- Some code that tries to use stickers, but they are not available in this score --!>
The correct usage of ng-contaner is to actually wrap it AROUND the elements that require the data you're fetching from the service.
In any case, I would not use the ng-container here at all. You use case is:
You have an observable of an array: Observable<ISticker[]>
You want to create an element for each of the ISticker objects.
For this you can combine async pipe together with *ngFor:
<app-home *ngFor="let sticker of shoppingCartItems$ | async">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
Separately from this, you want to peek at the length of the array to display an extra p tag.
Since you already have the access to the observable, you can easily do
<p>You have {{(shoppingCartItems$|async)?.length}} items in your bag</p>
<!-- OR -->
<p *ngIf="shoppingCartItems$|async as items">
You have {{items.length}} items in your bag
</p>
The problem with this is that now you're accessing shoppingCartItems$ TWICE: one time for the list of items, and one time to get the length. If you're getting this data from an API, this can easily result in TWO requests.
To combat this problem, you can use shareReplay operator, which will allow multiple subscribers to use the same value from a single observable:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]>;
constructor(private cartService: CartService) {
}
ngOnInit() {
this.shoppingCartItems$ = this.cartService.getItems()
.pipe(shareReplay(1));
}
}
Here's a stackblitz with this example, where you can see how this works.
Now you are free to use shoppingCartItems$ any number of times, and this won't cause any unintended behaviour.
On top of that, now we don't manually subscribe, which means that the async pipe will get rid of the subscription for us when the component is destroyed, preventing a potential memory leak.
Related
I have a component to which I pass an object as its input.
That object comes from an API, so it is not available right away. And the object returned from the API can also be empty.
In the template, I want to hide the value unless it is not an empty object.
So I have this in my component:
import { Component, Input, OnChanges, SimpleChanges } from "#angular/core";
import { BehaviorSubject, Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { not, isEmpty } from "ramda";
#Component({
selector: "hello",
template: `
{{ hasName | async }} {{ name | async }}
<h1 *ngIf="(hasName | async)">Hello {{ name | async }}!</h1>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: Observable<{} | { first: string }> = of({});
hasName: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
ngOnInit() {
this.name.pipe(
tap(name => console.log(name)),
// the actual logic to calculate the boolean is quite involved, but I
// have simplified it here for brevity.
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
}
The problem is that the console.log in the tap is never printed and the second tap is never run either, so the value for hasName is always false as set at the of the class.
I think what happens is that in OnInit, this.name is still of({}), and not the actual observable passed in through the template from the parent.
I am using of({}) because typing this.name as Observable<{} | { first: string}> | undefined leads to more boilerplate in the class to check it is indeed defined before using it.
How can I make this work in an idiomatic way?
Complete Stackblitz: https://stackblitz.com/edit/angular-ivy-gxqh2d?devtoolsheight=33&file=src/app/hello.component.ts
You can simplify this code a lot.
First off all, the TS definition.
{} | { first: string }
Can be done as
{ first?: string }
While it may look like I'm doing some neat picking here I'm not really. With the first option if you try to access variable.first you'll get an error saying it doesn't exist as the union cannot guarantee this. With the latter it's fine.
Then, the mock. When working with observables, try not to reassign observable. Here's how you can do what you where doing without reassigning:
public name$: Observable<{ first?: string }> = concat(
of({}),
interval(2000).pipe(
map(() => ({
first: `${Math.random()}`
}))
)
);
Finally, the child component. Passing observables as input is a code smell in 99% of the cases. Here all you want is a presentational component (also called dumb component).
This dumb component should not have any logic (as much as possible at least), and here's what you can do:
type Nil = null | undefined
#Component({
selector: "hello",
template: `
<h1 *ngIf="name?.first">Hello {{ name.first }}!</h1>
`
})
export class HelloComponent {
#Input() name: { first?: string } | Nil;
}
Which means that when you call this from the parent, you just do:
<hello [name]="name$ | async"></hello>
Here's an updated stackblitz: https://stackblitz.com/edit/angular-ivy-hnv23a?file=src%2Fapp%2Fapp.component.ts
If you want to react when the input value changes you can write your own setter:
export class HelloComponent {
#Input() set name(name: Observable<{} | { first: string }>) {
this.name$ = name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
name$: Observable<{} | { first: string }>;
...
}
Then in templates bind async to name$ instead.
Updated demo: https://stackblitz.com/edit/angular-ivy-7u7xkd?file=src%2Fapp%2Fhello.component.ts
I'd suggest to move your logic to app.component.ts instead of passing the whole observable into an input. This could help you to manage the value of that input and prevent assigning a value to it two times (you have assigned an empty object two times in your code - parent & child component).
Simplest thing to make your code work is just to subscribe to the observable you've created inside your ngOnInit:
this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
).subscribe();
In case you don't want to manage that (don't want to be worried about non destroyed observables) and you prefer to use the async pipe, you can just reassign this.name inside ngOnInit.
this.name = this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);
i am fetching some data via a angular service and takes about 2-3s to appear . so the HTML elements corresponding to that service also takes time to load . is there any way to show loading animation for that particular elements?
You need to track a loading state and have the template react accordingly.
If you're not using something like #ngrx/store for example, have a variable in your component.
isLoading = false
When fetching data, change that variable...
getSomeData(){
this.isLoading = true;
this.service.getSomeData().subscribe((data) => {
//... Do stuff with data
this.isLoading = false;
})
}
...then in your template...
<ng-container *ngIf="!isLoading; else loader">
<!-- Visible if not loading -->
</ng-container>
<ng-template #loader>
<!-- Loader element -->
</ng-templtate>
Be aware this is a VERY simple example of this and is often better handle via some sort of state management.
you can use the finalize operator from RxJs
https://www.learnrxjs.io/learn-rxjs/operators/utility/finalize
getData() {
this.showLoader = true;
this.service.getYourObs().pipe(finalize(() => this.showLoader = false)).subscribe(...);
}
All you need to know is whether the request is finished or not and set loading flag according to it.
AppComponent HTML
<ng-container>
<loading-el *ngIf = "loading"></loading-el>
<main-components *ngIf = "!loading"></main-components>
</ng-container>
Appcomponent.ts
loading = false;
getRecord() {
this.loading = true;
this.http.get(rec => {
this.loading = false;
...operation...
})
}
This is a pretty standard requirement and you have some options.
The simplest is to create a boolean property on your component and set it to true in the ngOnit hook. Here is a link that steps through it.
How to show spinner in angular 6
...as an alternative you could write an angular resolver to get your data prior to your component loading.
https://dzone.com/articles/understanding-angular-route-resolvers-by-example#:~:text=A%20Resolver%20is%20a%20class,may%20also%20like%3A%20Angular%20Resolvers.
Both require a loader graphic of some kind, the resolver allowing you to handle loading at an application level, which stops duplication in your components.
You actually don't need to manage a flag at the component level.
Use observables.
import { Component, OnInit } from '#angular/core';
import { from, Observable } from 'rxjs';
class Item {
constructor(public name = 'New Item') {}
}
#Component({
selector: 'my-app',
template: `
<button (click)="loadItems()">Load</button>
<hr>
<ng-container *ngIf="items$ | async as items; else loading">
<div *ngFor="let item of items; let index = index">
{{index+1}}. {{ item.name }}
</div>
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
`,
})
export class AppComponent implements OnInit {
items$: Observable<Item[]>;
ngOnInit() {
this.loadItems();
}
private loadItems() {
// Simulate HTTP request
const promise: Promise<Item[]> = new Promise((resolve) => {
setTimeout(() => {
resolve([new Item(), new Item(), new Item()]);
}, 750);
});
// Where the magic happens
// Reassign the observable, which will trigger the else
// block in the template and display the loader.
// Once it completes, it will display the list as normal.
this.items$ = from(promise);
}
}
I have a service which has following method:
room-service.ts
#Injectable()
export class RoomService {
private readonly _equipment: BehaviorSubject<EquipmentDto[]> = new BehaviorSubject([]);
public equipment$ = this._equipment.asObservable();
getEquipmentForRoom(roomId: number) {
this.restService.getEquipmentForRoom(roomId).subscribe(res => {
this._equipment.next(res);
});
}
room-component.ts:
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable < EquipmentDto[] > ;
equipmentList: Array < EquipmentDto > ;
constructor(private equipmentService: EquipmentService) {}
ngOnInit() {
this.equipment$ = this.equipmentService.equipment$;
this.equipmentService.getEquipmentForRoom(this.room.id);
this.equipment$.subscribe(items => {
this.equipmentList = items;
});
room-component.html
<div *ngFor="let eq of room.equipmentList">
<!-- list my equipment here -->
</div>
Now I have a parent component which contains multiple Room Components (those are added programmatically based on the amount of rooms). Anyway, list of equipment is the same for each of the rooms. It looks like once subscribed, the data in first components is overwritten by the component created as the last one.
My question is, how can I get a proper data for each of the rooms using the observable from my service?
You can use this approach with single BehaviorSubject only when your data is the only source of this data.
Instead, you can change your getEquipmentForRoom(roomId: number) like this:
getEquipmentForRoom(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
And then subscribe to it in the compoment:
this.equipmentService.getEquipmentForRoom(this.room.id).subscribe(items => {
this.equipmentList = items;
});
And I agree with Alexander, this component should be dumb as possible.
try to build pipes instead of subscriptions
#Injectable()
export class RoomService {
// return an observable.
getEquipmentForRoom$(roomId: number) {
return this.restService.getEquipmentForRoom(roomId);
}
#Component()
export class RoomsComponent implements OnInit {
#Input() room: RoomEntity;
equipment$: Observable<EquipmentDto[]>;
constructor(private equipmentService: EquipmentService){}
ngOnInit() {
// simply share observable.
this.equipment$ = this.equipmentService.getEquipmentForRoom$(this.room.id);
});
<div *ngFor="let eq of equipment$ | async"> <!-- add async here -->
<!-- list my equipment here -->
</div>
You can fetch the data once in your parent and pass the data to your child components.
That child component (RoomsComponent) should be dumb and not doing requests
Adding onto Alexander’s answer.
The problem is that you are subscribing to the same BehaviourSubject for all the components and the components are taking the latest roomData that is emitted by the BehaviourSubject, which would be the last RoomComponent.
It would work if the BehaviuorSubject holds all the rooms which are fetched by the parent component, and passing each room data to each RoomComponent using #Input.
I am trying to remove duplicate records from a *ngfor loop and leave only the record with the most clicks for that record.
The objective is to show click-through URLs from the user, but currently, when a new record for the same URL has been created, it displays in the list. See the image below:
The clicks are working as expected, but the list will become illegible after a while. I'm trying to show e.g Product: Away Shirt, Click through URL https://blablabla Ad Clicks: 6, as this is the most recent click number I need to display. Records showing the same Product which have old ad click data needs to be hidden or removed from the array. There are currently records with the same product name, URL and click data which is increasing with each new click. I could place a date when the record was created, but this seems a little crass and unrefined. I would rather just show the most up to date record.
I have tried to create a filter, where the filter looks to remove duplicates from the get request which creates a variable this.commissions from the response, but each filter approach doesn't work and returns a series of empty arrays.
Edited: Using Moxxi's solution and adding some returns to the component, the view is now binding something - which is 'false', but it is binding something:
analytics.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { environment } from 'src/app/environments/environments';
import { throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { Article } from './article';
#Injectable({
providedIn: 'root'
})
export class AnalyticsService {
article_url = environment.api_url + 'text_image_templates/rows';
commissions_url = environment.api_url + 'commissions/rows';
constructor(private http: HttpClient) { }
getAllArticles(){
return this.http.get<{data: Article[]}>(this.article_url)
.pipe(
retry(1),
catchError(this.handleError),
);
}
getAllCommissionData(): Observable<Card[]>{
return this.http.get<Card[]>(this.commissions_url)
.pipe(
retry(1),
catchError(this.handleError),
)
}
handleError(error) {
let errorMessage = '';
if (error.error) {
errorMessage = error.error.message;
} else {
errorMessage = error;
}
return throwError(errorMessage);
}
}
card class
export class Card {
url: string;
page_url: string;
page_type: string;
clicks: number;
}
click-cards.component.ts
import { Component, OnInit } from '#angular/core';
import { Commission } from '../commission';
import { AnalyticsService } from '../analytics.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as _ from 'lodash';
import { Card } from '../card';
#Component({
selector: 'app-click-cards',
templateUrl: './click-cards.component.html',
styleUrls: ['./click-cards.component.scss']
})
export class ClickCardsComponent implements OnInit {
commissions$: Observable<any>;
constructor(private analyticsService: AnalyticsService) {}
ngOnInit() {
this.getCommissions();
}
getCommissions(){
this.commissions$ = this.analyticsService.getAllCommissionData().pipe(
map((commisions: Card[]) => _.uniqBy(commisions.sort((a, b) => b.clicks - a.clicks), commission => commission.page_url)),
map((commissions: Card[]) => _.groupBy(commissions, commission => commission.page_type)),
)
}
}
click-cards.component.html
<ng-container *ngIf="commissions$ | async as commissions">
<ng-container *ngFor="let type of ['home', 'article', 'products']">
<h4>{{ type | titlecase }}</h4>
<p *ngIf="!commissions[type]">No {{ type }} Commissions Logged Yet</p>
<ul *ngFor="let card of commissions[type]">
<app-click-card [card]="card"></app-click-card>
</ul>
</ng-container>
</ng-container>
click-card.component.html
<ng-container *ngIf="card">
<li>
<ul>
<li><strong>Click Origin:</strong> {{ card.page_url }}</li>
<li><strong>Click Through Url:</strong> {{ card.url }}</li>
<li *ngIf="card.clicks"><strong>Ad Clicks:</strong> {{ card.clicks }}</li>
<li *ngIf="!card.clicks">No Ad Clicks Yet</li>
</ul>
</li>
</ng-container>
Is this relating to the fact I am using child-components in the loop? Do I need to do something inside the child-component.ts? I am a little stumped as to what my next step is?
Has anyone come across this issue before?
Create below Custom Pipe .Here I set 'name' column for duplicate record, you can set your own column on which you want to remove duplication.
import { Pipe, PipeTransform } from '#angular/core';
import * as _ from 'lodash';
#Pipe({
name: 'unique',
pure: false
})
export class UniquePipe implements PipeTransform {
transform(value: any): any{
if(value!== undefined && value!== null){
return _.uniqBy(value, 'name');
}
return value;
}
}
and apply on your *ngFor .
<ng-container *ngFor="let card of commissions | unique">
<ul>
<ng-container *ngIf="card.page_type === 'products'">
<app-click-card [card]="card"></app-click-card>
</ng-container>
</ul>
</ng-container>
let me know If you have any query.Thanks.
Edit: see Stackblitz here: https://stackblitz.com/edit/angular-3kchji
Don't do the filter pipe and don't subscribe within the component.
Better: store Observable, use rxjs operators to remove the duplicates and use async pipe.
Async pipe in your template
<ng-container *ngIf="commissions$ | async as commissions">
<h4>Home</h4>
<p *ngIf="!commissions['home']">No home Commissions Logged Yet</p>
<ul *ngFor="let card of commissions['home']">
<app-click-card [card]="card"></app-click-card>
</ul>
<h4>Articles</h4>
<p *ngIf="!commissions['article']">No article Commissions Logged Yet</p>
<ul *ngFor="let card of commissions['article']">
<app-click-card [card]="card"></app-click-card>
</ul>
<h4>Products</h4>
<p *ngIf="!commissions['products']">No product Commissions Logged Yet</p>
<ul *ngFor="let card of commissions['products']">
<app-click-card [card]="card"></app-click-card>
</ul>
</ng-container>
And your component
export class ClickCardsComponent implements OnInit {
commissions$: Observable<any>;
constructor(private analyticsService: AnalyticsService) { }
ngOnInit() {
this.getCommissions();
}
getCommissions(){
this.commissions$ = this.analyticsService.getAllCommissionData().pipe(
map((commissions: Commission[]) => {
/* your logic to remove duplicates of the array */
}),
// below is extended answer
map((commissions: Commission[]) => {
_.groupBy(commissions, commission => commission.page_type)
})
)
}
}
Beyond that you could also store the types you want to display within an array and loop it
<ng-container *ngIf="commissions$ | async as commissions">
<ng-container *ngFor="let type of ['home', 'article', 'products']">
<h4>{{ type | titlecase }}</h4>
<p *ngIf="!commissions[type]">No {{ type }} Commissions Logged Yet</p>
<ul *ngFor="let card of commissions[type]">
<app-click-card [card]="card"></app-click-card>
</ul>
</ng-container>
</ng-container>
And with this being done you maybe also want to spare your app-click-card component and add it directly in the ul tag.
You can clean up your commisions array from duplicates in ts like below:
use the primary key (id) of each element to permit duplicates suppression
removeLabelDuplicates(array: any) {
return arr.
filter((item, your_commision_id, self) =>
your_commision_id === self.findIndex((t) => (
t.your_commision_id=== item.your_commision_id
))
)
}
#MoxxiManagarm's answer is correct but if you are not sure about implementing observables or anything, remove your pipe, and while you subscribe to the returned data in click-cards.component.ts, filter your response data through response.data.reduce. You will need to maintain a separate list of whichever url's are already there so as to eliminate duplicates (implement your duplicate detection logic). If you encounter any more problem, showing the data you are receiving and what part of data you want to be unique would certainly help, but don't share any data you would mind having people access to. If possible, just share the dummy structure of the data.
I have a variable in my app.component.ts file that is passed in from a controller via my services.ts file.
As far as I can see, the Object property passed to the Component via the Subject is working, because I can console.log(this.selectedContact.name) and It will log to the console perfectly, which would mean that it is correctly being stored into the variable selectedContact: any;. But I cannot seem to bind this to my HTML and I am curious as to what I may be missing.
Here is my app.component.ts file for the view I'm attempting to bind to:
import { Component, OnInit } from '#angular/core';
import { ContactsListComponent } from '../contacts-list/contacts-list.component';
import { ApiService } from '../api.service';
#Component({
selector: 'app-contact-details',
templateUrl: './contact-details.component.html',
styleUrls: ['./contact-details.component.scss']
})
export class ContactDetailsComponent implements OnInit {
selectedContact: any;
constructor(private _apiService: ApiService) { }
ngOnInit() { this.showContact()}
showContact() {
this._apiService.newContactSubject.subscribe(
contact => {
this.selectedContact = contact;
console.log(this.selectedContact.name);
});
}
}
Here is my HTML file (The view I am attempting to bind to.:
<div id= 'contact-card' >
<header>
<button routerLink='/home'>< Contacts</button>
</header>
<section id= card-container>
<img id ="largeContactPhoto" onerror="onerror=null;src='../assets/User Large/User — Large.png';"
src={{selectedContact.largeImageURL}} />
<h1> {{selectedContact.name}} </h1>
<h2> {{selectedContact.companyName}} </h2>
<ul>
<hr>
<li class="contact-data">
<h3> Phone: </h3>
{{selectedContact.phone.home}}
<hr>
</li>
<li class="contact-data">
<h3> Phone: </h3>
{{selectedContact.phone.mobile}}
<hr>
</li>
<li class="contact-data">
<h3> Phone: </h3>
{{selectedContact.phone.work}}
<hr>
</li>
<li class="contact-data">
<h3> Address: </h3>
{{selectedContact.address.street}}<br>
{{selectedContact.address.city}},{{selectedContact.address.state}} {{selectedContact.address.zipcode}}
<hr>
</li>
<li class="contact-data">
<h3> Birthdate: </h3>
{{selectedContact.birthdate}}
<hr>
</li>
<li class="contact-data">
<h3> Email: </h3>
{{selectedContact.email}}
<hr>
</li>
</ul>
</section>
</div>
app.service.ts (where the click event data is being passed to app.component.ts:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
#Injectable()
export class ApiService {
//API URL
private url: string = 'assets/api/contacts.json';
public newContactSubject = new Subject<any>();
//Initialize HttpClient for request
constructor(private _http: Http) { }
//Pull JSON data from REST API
getContacts(): Observable<any> {
return this._http.get(this.url)
.map((response: Response) => response.json());
}
openSelectedContact(data) {
this.newContactSubject.next(data);
}
}
Sibling Component THIS IS WHERE THE CLICK EVENT HAPPENS* :
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { ApiService } from '../api.service';
#Component({
selector: 'app-contacts-list',
templateUrl: './contacts-list.component.html',
styleUrls: ['./contacts-list.component.scss']
})
export class ContactsListComponent implements OnInit {
title : string = 'Contacts';
sortedFavorites: any[] = [];
sortedContacts: any[] = [];
errorMessage: string;
constructor (private _apiService: ApiService, private router: Router) {}
ngOnInit(){ this.getContacts()}
getContacts() {
this._apiService.getContacts()
.subscribe(
(contacts) => {
//Sort JSON Object Alphabetically
contacts.sort((a , b) => {
if (a.name > b.name) {return 1};
if (a.name < b.name) {return -1};
return 0;
})
//Build new Sorted Arrays
contacts.forEach( (item) => {
if (item.isFavorite) {
this.sortedFavorites.push(item);
} else {
this.sortedContacts.push(item);
}
});
});
}
openFavorite($event, i) { <--Click event coming in from HTML template
let selectedFavorite = this.sortedFavorites[i];
this._apiService.openSelectedContact(selectedFavorite); <--Routing this data to services.ts file
this.router.navigate(['/details']); <--Navigate to detailed view to see selected contact.
};
}
This binding will work with any other test variables that i put into my app.component.ts file and they get bounded to the page. I feel like this is the right syntax for it to be properly binding the object.properties, but it doesn't seem to be working.
What might I be missing on this? Thanks a lot in advance!!
EDIT One: I feel like I should also add that this exact same code was working before when I had my component routed to this component. It was only when I moved my files to a new component and created a different new that this started happening. Maybe it is isn’t imported correctly somewhere?
EDIT TWO: I have refactored the code in the original post and corrected my TypeScript declarations and removed this from my html template. I have placed my console.log(this.selectedContact.name) function inside my showContact() method and it does log the correct name once the name in the contacts list is clicked along side this error: ERROR TypeError: Cannot read property 'largeImageURL' of undefined. So it is logging this.selectedContact.name correctly and that variable isn't defined anywhere else in my application. I have added my app.service.ts file as well as my sibling component.ts file just to put this in context where the click event happens. I want the click event data to go from Sibling component --> Api Service.ts file --> Sibling component on a click event. This is very confusing to me why this isn't happening.
EDIT THREE
One other issue that I have noticed is that whenever I attempt to use the safe navigation operator to resolve this, my highlighting changes in my html template, making me think that it is breaking it.
versus this:
Notice the change in the </h1> highlighting? This has subsiquently made every open and closing tag show up in grey instead of the red now. I have never attempted to use the safe navigation operator, so I am unsure as to whether this is normal. I have tried it with different spacing around the {{ and }} and still no luck.
Are you expecting an array or an object? You should type exactly what you are getting from your service.
Here you mention an array:
selectedContact: any[] = [];
Here you mention an object.
<h1> {{this.selectedContact.name}} </h1>
Give a shot in your template with the json pipe.
{{ selectedContact | json }}
This will help you debug.
Also, because your data is displayed on the browser before it is received by your service, you will get an undefined variable. Either use ? or an *ngIf. (With the *ngIf, don't define your selectedContact as an array to start with)
<div id= 'contact-card' *ngIf="selectedContact">
or
<h1> {{this.selectedContact?.name}} </h1>