Angular 2 - Manipulating rxjs Observable in a view - javascript

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>

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 can not get user data form sessionstorage

sorry about my english.
I use sessionstorage for keeping data. In sessionstorage have data enter image description here
but in html, not showing data form sessionstorage. when I get only {{currentUser}} in html show like this enter image description here
mycode services
import { Injectable } from '#angular/core';
const USER_KEY = 'auth-user';
#Injectable({
providedIn: 'root'
})
export class TokenStorageService {
constructor() { }
signOut(): void {
window.sessionStorage.clear();
}
public saveUser(user: any): void {
window.sessionStorage.removeItem(USER_KEY);
window.sessionStorage.setItem(USER_KEY, JSON.stringify(user));
}
public getUser(): any {
const user = window.sessionStorage.getItem(USER_KEY);
if (user) {
return JSON.parse(user);
}
return {};
}
}
html
<div class="container" *ngIf="currentUser; else loggedOut">
<header class="jumbotron">
<h3>
<strong>{{ currentUser.employee_code }}</strong> Profile
</h3>
</header>
<p>
<strong>Token:</strong>
{{ currentUser.accessToken.substring(0, 20) }} ...
{{ currentUser.accessToken.substr(currentUser.accessToken.length - 20) }}
</p>
<p>
<strong>Emp:</strong>
{{ currentUser }}
</p>
</div>
{{ currentUser }}
<ng-template #loggedOut>
Please login.
</ng-template>
and component
import { Component, OnInit } from '#angular/core';
import { TokenStorageService } from '../../../services/token-storage.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
currentUser: any;
constructor(private token: TokenStorageService) { }
ngOnInit(): void {
this.currentUser = this.token.getUser();
console.log(this.currentUser = this.token.getUser())
}
}
how i can do to use please help me
this image for {{ currentUser|json}}
{{ currentUser|json}}
In TokenStorageService change getUser() method.
public getUser(): any {
return JSON.parse(window.sessionStorage.getItem(USER_KEY));
}
In HTML you are printing {{ currentUser }} Which will be an object. You need to specify the property of object.
Note: If you want to see the object in html use json pipe. ({{ currentUser | json }})
if I could see it correctly in your attached image, It is an array not an object, so you will need to use it like currentUser[0], to check it on your own please use
{{ currentUser | json}} in HTML it will show the exact content.
Hi, I have created a similar app with the source code provided by you stackblitz link here. I have made some modifications to mimic the login scenario.
You should be able to see the data on the initial load of the data. As the component is being initialized for the first time. But whereas when you change the user with the change user button. Although the session storage data changes. You won't be able to see the new data. In order to see these kinds of dynamic changes, you need to make use of Observables/Subjects.
Edit 1: The issue here was that the key & value stored in the local storage as stored as strings. So while storing we have to do JSON.Stringify() and JSON.Parse() while extracting back. This is explained in detail here.

How I can deselect button in angular?

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

How do you query and display the authenticated user's order from the Firebase Realtime Database using AngularFire in an Angular app?

This is the function I am using to insert orders into the database, which works fine.
async createPackage(){
const itemsRef = this.afDatabase.database.ref(`delivery orders/${this.uid}`);
const userId =
itemsRef.push({packageName: this.packageName, packageSize: this.packageSize, packageDescription: this.packageDescription, packageFrom: this.packageFrom, packageTo: this.packageTo, deliveryDate: this.deliveryDate, receiverNumber: this.receiverNumber, paymentOption: this.paymentOption, UID: this.uid})
this.packageName = '';
this.packageDescription = '';
this.packageFrom = '';
this.packageTo = '';
this.deliveryDate = '';
this.paymentOption = '';
this.receiverNumber = '';
this.packageSize = '';
this.showAlert('Your package delivery order has been successfully taken.', 'You will be contacted by one of our Administrators soon.');
}
here is a screenshot of how it's been structured in the database, the parent node is the user uid for each user, while the child node is the id for each order by each user.
the problem I am having is how to query and display each user's order separately on their dashboard differently on the front end.
For context, the complete files for this answer can be found here: https://gist.github.com/nclarx/ef581b0e1a95a2d43531411fe91a9814
To Query the User's Orders
To query the user's data you need to use a function similar to the following:
getCurrentOrder(): Observable<Order[] | never> {
return this.afAuth.authState // authState is an observable
.pipe( // use pipe
switchMap((user) => { // switchMap gets authState and then lets you return a different observable
// The following returns an observable call to the real-time database:
return user ? this.afDatabase.list<Order>(`delivery-orders/${user.uid}`).valueChanges() // if the user is authenticated an observable with the Orders is returned
: EMPTY; // if the user is not authenticated an empty observable is returned
})
);
}
This isn't an ideal way to do authentication in a larger application. I suggest you look at this video on Authentication with AngularFire https://www.youtube.com/watch?v=qP5zw7fjQgo and https://fireship.io/lessons/angularfire-google-oauth/ to create an AuthService that can be used across your application.
Displaying the Orders
To display that information in an Angular component using Observables from AngularFire I suggest the following structure/pattern:
OrderService - contains methods for accessing the database (generate the service with the Angular CLI)
AppComponent - has OrderService injected and calls getCurrentUserOrder() when the component initialises ngOnInit()
Template: app.component.html - the template which uses the async pipe in a *ngFor directive to subscribe/unsubscribe to the observable automatically.
Handing an observable to the template and using the async pipe to subscribe to it is good practice because it means that you do not need to subscribe and unsubscribe from an observable manually.
The files can be found in their entirety here: https://gist.github.com/nclarx/ef581b0e1a95a2d43531411fe91a9814
See the comments in the code for important points about how this works.
The Service: OrderService.ts
import {Injectable} from '#angular/core';
import {EMPTY, Observable} from 'rxjs';
import {AngularFireAuth} from '#angular/fire/auth';
import {switchMap} from 'rxjs/operators';
import {AngularFireDatabase} from '#angular/fire/database';
export interface Order {
// write interfaces for all of your objects and use them
// when defining variables and function return types.
packageName: string;
packageSize: number;
packageDescription: string;
packageFrom: string;
packageTo: string;
deliveryDate: Date;
receiverNumber: number;
paymentOption: string;
UID: string;
}
export class OrderService {
constructor(private afAuth: AngularFireAuth, private afDatabase: AngularFireDatabase) {
}
getCurrentOrder(): Observable<Order[] | never> { // note the use of the interface: Order[], which means returning an array of Orders
return this.afAuth.authState // authState is an observable
.pipe( // use pipe
switchMap((user) => { // switchMap gets authState and then lets you return a different observable
// The following returns an observable call to the real-time database:
return user ? this.afDatabase.list<Order>(`delivery-orders/${user.uid}`).valueChanges() // if the user is authenticated an observable with the Orders is returned
: EMPTY; // if the user is not authenticated an empty observable is returned
// NOTE: this observable is not called until it is subscribed to in the template using the `async pipe`, see
// `app.component.html` where it has `*ngFor="let order of orders$ | async"` <== this is what kicks off the request to the database
})
);
}
}
The Component: app.component.ts
import {Component, OnInit} from '#angular/core';
import {Order, OrderService} from './order.service';
import {Observable} from 'rxjs';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'ng-fire-so-qtn';
orders$: Observable<Order[]>; // property to hold the observable which will have your array of Orders
constructor(public orderService: OrderService) { // inject the OrderService into the component
}
ngOnInit() {
this.orders$ = this.orderService.getCurrentOrder();
// When this component is initialised it will set the `orders$` property to the `Observable<Order[]>` so it is available in the template
}
}
The Template: app.component.html
<section>
<ul *ngIf="orders$ | async"> <!-- The *ngIf will hide the whole list until the data has arrived-->
<li *ngFor="let order of orders$ | async"> <!-- The *ngFor will loop over and create list items for the orders once the data has arrived-->
{{order.packageName}}: {{order.packageDescription}}
</li>
</ul>
</section>
Thanks everyone, i finally was able to query each user's submission from the firebase realtime database with angular with this few lines of code.
getCurrentUserOrder() {
return this.afAuth.authState.subscribe(user => {
if(user) {
this.userId = user.uid;
console.log(user.uid)
console.log(user.email)
}
this.afDatabase.list(`delivery orders/${this.userId}`).valueChanges().subscribe(
data => {
console.log(data);
this.orders = data;
}
);
});
after importing the AngularFireDatabase and the AngularFireAuth inside the component.On the frontend,
<section *ngFor="let order of orders">
<ion-card>
<ion-item>
<ion-icon name="cube" slot="end"></ion-icon>
<ion-label>{{order.packageName}}</ion-label>
</ion-item>
<ion-card-content>
</ion-card-content>
</ion-card>
</section>
this solves the entire problems completely.

Display ContentFul RichText in Angular

I have a blog feed in my Angular App connected with Contentful. Thanks to the Contentful javascript sdk.
https://www.contentful.com/developers/docs/javascript/tutorials/using-contentful-in-an-angular-project/
I'm trying to display the Title and the Text field. Here is my code:
import { Component, OnInit } from '#angular/core';
import {Observable} from 'rxjs';
import {ContentfulService} from '../../services/contentful/contentful.service';
import { Entry } from 'contentful';
#Component({
selector: 'app-blog',
templateUrl: './blog.component.html',
styleUrls: ['./blog.component.scss']
})
export class BlogComponent implements OnInit {
private posts: Entry<any>[] = [];
constructor(private postService: ContentfulService) {}
ngOnInit() {
this.postService.getPosts()
.then(posts => {
this.posts = posts;
console.log(this.posts);
});
}
}
And the html:
<div *ngFor="let post of posts">
{{ post.fields.title }}
<div>{{ post.fields.text }}</div>
</div>
The title field is displayed well because it is just a string field but the text field is RichText and display [object Object].
Indeed it contain several object. It seems like the Object is divided in several pieces.
https://www.contentful.com/developers/docs/concepts/rich-text/
Does somebody have already display Contentful RichText in an Angular App ?
Is there a specific way to do it?
First, you must install rich-text-html-renderer from your terminal:
npm install #contentful/rich-text-html-renderer
Then, you can import it from your Component:
import { documentToHtmlString } from '#contentful/rich-text-html-renderer';
and use it, simply like that:
_returnHtmlFromRichText(richText) {
if (richText === undefined || richText === null || richText.nodeType !== 'document') {
return '<p>Error</p>';
}
return documentToHtmlString(richText);
}
Finally, 'call the function' from your html like so:
<div [innerHtml]="_returnHtmlFromRichText(post.fields.text)">
</div>
You can also add some options to customise your rich text, more information here. Also, you should code a function similar to _returnHtmlFromRichText in your Contentful service to be able to reuse it later.
I created an Angular library that can render rich text using Angular components: https://github.com/kgajera/ngx-contentful-rich-text
Why use this over #contentful/rich-text-html-renderer? If you need to customize the default mark-up, it allows you to use your Angular components which you can't do using the documentToHtmlString function and [innerHTML].

Categories