i have a deckbuilder, a decklist component that shows the list of decks, and when i click on a deck the proper deck-details component is loaded using the deck's id with routing. This is the deck-details part where i use route.
ngOnInit(){
this.decks = this.deckService.getDecks(); // this just returns the array of decks
this.id = this.route.snapshot.params["id"];
this.paramsSubscription = this.route.params.subscribe((params: Params) => {
this.id = params["id"];
console.log(this.id);
this.deck = this.decks.find((deck) => deck.id === this.id);
console.log("deck-detail deck", this.deck);
}
and this is the app-routing.module
const appRoutes: Routes = [
{ path: '', component: CardListComponent },
{ path: 'decks', component: DeckListComponent, children: [
{ path: ':id/:deckName', component: DeckDetailsComponent }
] },
{ path: 'createDeck', component: CreateDeckComponent },
];
#NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
so the url become something like this when i click on a deck: localhost:4200/decks/id/deckName.
If page is reloaded when the url is like this, deck-detail component is not reloaded with the page, and i get this error
TypeError: Cannot read property 'deckName' of undefined
at DeckDetailsComponent_Template
What i'm doing wrong? there is a component lifecycle problem? how can i avoid decks to be undefined when i reload the page?
this is the whole deck-details component
import { Card } from "./../../card/card.model";
import { Deck } from "./../../deck/deck.model";
import { Component, OnInit, Input } from "#angular/core";
import { DeckService } from "src/app/deck/deck.service";
import { ActivatedRoute, Params, Router } from "#angular/router";
import { Subscription } from "rxjs";
import { DataStorageService } from "src/app/shared/data-storage.service";
import { CardService } from "src/app/card/card.service";
import { FormControl, FormGroup } from "#angular/forms";
#Component({
selector: "app-deck-details",
templateUrl: "./deck-details.component.html",
styleUrls: ["./deck-details.component.scss"],
})
export class DeckDetailsComponent implements OnInit {
//#Input() deck: Deck;
paramsSubscription: Subscription;
id: number;
decks: Deck[];
deck: Deck;
cards: Card[];
selectedClassCards: Card[];
classes = [
"Priest",
"Mage",
"Shaman",
"Rogue",
"Warrior",
"Warlock",
"Druid",
"Paladin",
];
addCardForm: FormGroup;
constructor(
private deckService: DeckService,
private cardService: CardService,
private route: ActivatedRoute,
private dataStorageService: DataStorageService
) {}
ngOnInit() {
this.addCardForm = new FormGroup({
deckCards: new FormControl(),
});
this.cards = this.cardService.getCards();
this.decks = this.deckService.getDecks();
this.id = this.route.snapshot.params["id"];
this.paramsSubscription = this.route.params.subscribe((params: Params) => {
this.id = params["id"];
this.deck = this.decks.find((deck) => deck.id === this.id);
console.log("deck-detail deck", this.deck);
this.selectedClassCards = this.cards.filter(
(card) => card.hero === this.deck.deckClass || card.hero === "Neutral"
);
});
}
onDeleteCard(i) {
this.deckService.deleteCard(this.deck, this.deck.deckCards[i]);
//this.deckService.cardsChanged.next(this.deck.deckCards.slice());
console.log(this.deck);
}
onAddCard() {
this.deckService.addCard(this.deck, this.addCardForm.value.deckCards);
console.log(this.deck);
}
onCardsEdit() {
this.dataStorageService.storeCards(
this.decks.indexOf(this.deck),
this.deck.deckCards
);
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
}
and this is the DeckService
import { DataStorageService } from './../shared/data-storage.service';
import { Deck } from './deck.model';
import { Card } from '../card/card.model';
import {EventEmitter, Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Subject } from 'rxjs';
#Injectable()
export class DeckService {
deckSelected = new EventEmitter<Deck>();
decksChanged = new Subject<Deck[]>();
cardsChanged = new Subject<Card[]>();
decks: Deck[] = [];
remainingDecks: Deck[];
constructor(/* private http: HttpClient, */ /* private dataStorageService: DataStorageService */) {}
setDecks(decks: Deck[]) {
this.decks = decks;
this.decksChanged.next(this.decks.slice());
}
getDecks() {
if (this.decks) {
return this.decks.slice();
}
}
addCard(deck: Deck, card: Card){
deck.deckCards.push(card);
}
deleteCard(deck: Deck, card: Card){
deck.deckCards.splice( deck.deckCards.indexOf(card), 1);
}
addNewDeck(deck: Deck){
this.decks.push(deck);
this.decksChanged.next(this.decks.slice());
};
deleteDeck(id: number) {
this.remainingDecks = this.decks.filter(
deck => deck.id !== id
)
this.decks = this.remainingDecks;
this.decksChanged.next(this.decks.slice());
}
}
and this is where i store and fetch decks with fire base and httpClient
import { DeckService } from "./../deck/deck.service";
import { CardService } from "./../card/card.service";
import { Deck } from "./../deck/deck.model";
import { Card } from "./../card/card.model";
import { Injectable } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { map, tap } from "rxjs/operators";
#Injectable({ providedIn: "root" })
export class DataStorageService {
constructor(private http: HttpClient, private deckService: DeckService) {}
storeDecks() {
const decks = this.deckService.getDecks();
this.http
.put("https://ang-cards.firebaseio.com/decks.json", decks)
.subscribe((response) => {
console.log(response);
console.log("stored");
});
}
fetchDecks() {
return this.http
.get<Deck[]>("https://ang-cards.firebaseio.com/decks.json")
.subscribe((decks) => {
decks
? this.deckService.setDecks(decks)
: this.deckService.setDecks([]);
console.log("fetching", decks);
});
}
storeCards(i: number, cards: Card[]){
this.http
.put("https://ang-cards.firebaseio.com/decks/" + i + "/deckCards.json", cards)
.subscribe((response) => {
console.log(response);
console.log("cards stored");
});
}
}
for info this is the deck-detail component html template
<div class="container">
<div class="row">
<div class="deck-details " style="border:solid black">
<!-- <img
[src]="'../../assets/img/' + deck.deckClass + '.png'"
alt="{{ deck.deckName }}"
class="img-responsive"
style="height: 200px;"
/> -->
<h1 contenteditable="true">{{ deck.deckName }} : {{ deck.deckClass }}</h1>
<div *ngFor="let card of deck.deckCards; let i = index" >
<span
[ngClass]="{
common: card.rarity == 'common',
rare: card.rarity == 'rare',
epic: card.rarity == 'epic',
legendary: card.rarity == 'legendary'
}"
>
{{ card.name }}
</span>
<h4 class="inlineh4">: {{ card.manaCost }} mana</h4>
<button class="btn btn-danger" (click) = "onDeleteCard(i)" >delete</button>
<!-- <img [src]="card.imagePath" alt="" style="height: 20px;" /> -->
</div>
<hr>
<label for="deckCards">Add a Card</label>
<form [formGroup]="addCardForm">
<select formControlName="deckCards" id="deckCards" name="deckCards">
<option *ngFor="let card of selectedClassCards" [ngValue]="card">
{{ card.name }}
</option>
</select>
<button (click)="onAddCard()" type="button" class="btn btn-primary">
Add Card
</button>
</form>
<hr>
<button class="btn btn-primary"(click) = "onCardsEdit()" >Save changes</button>
</div>
</div>
</div>
Related
i'am in the process of creating a shoping cart but i have this weird probleme were the Id of the basket always null
basket.ts
import { v4 as uuidv4 } from 'uuid';
export interface IBasket {
id: string;
items: IBasketItem[];
}
export interface IBasketItem {
id: number;
productName: string;
price: number;
quantity: number;
pictureUrl: string;
brand: string;
type: string;
}
export class Basket implements IBasket {
id: string = uuidv4();
items: IBasketItem[] = [];
}
basket service
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { BehaviorSubject, map } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Basket, IBasket, IBasketItem } from '../shared/Models/basket';
import { IProduct } from '../shared/Models/product';
#Injectable({
providedIn: 'root',
})
export class BasketService {
baseUrl = environment.apiUrl;
private basketSource = new BehaviorSubject<IBasket>(null);
basket$ = this.basketSource.asObservable();
constructor(private http: HttpClient) {}
getBasket(id: string) {
return this.http.get<IBasket>(this.baseUrl + 'basket?id=' + id).pipe(
map((basket: IBasket) => {
this.basketSource.next(basket);
})
);
}
setBasket(basket: IBasket) {
return this.http.post<IBasket>(this.baseUrl + 'basket', basket).subscribe(
(response: IBasket) => this.basketSource.next(response),
(error) => {
console.log(error);
}
);
}
getCurrentBasketValue() {
return this.basketSource.getValue();
}
addItemToBasket(item: IProduct, quantity = 1) {
const itemToAdd: IBasketItem = this.mapProductItemToBasketItem(
item,
quantity
);
const basket = this.getCurrentBasketValue() ?? this.createBasket();
basket.items = this.addOrUpdateItem(basket.items, itemToAdd, quantity);
console.log(basket);
// this.setBasket(basket);
}
private addOrUpdateItem(
items: IBasketItem[],
itemToAdd: IBasketItem,
quantity: number
): IBasketItem[] {
const index = items.findIndex((i) => i.id === itemToAdd.id);
if (index === -1) {
itemToAdd.quantity = quantity;
items.push(itemToAdd);
} else {
items[index].quantity += quantity;
}
return items;
}
private createBasket(): IBasket {
const basket = new Basket();
console.log('ezeze');
localStorage.setItem('basket_id', basket.id);
return basket;
}
private mapProductItemToBasketItem(
item: IProduct,
quantity: number
): IBasketItem {
return {
id: item.id,
productName: item.name,
price: item.price,
pictureUrl: item.pictureUrl,
quantity,
brand: item.productBrand,
type: item.productType,
};
}
}
product-item.ts
import { Component, OnInit, Input } from '#angular/core';
import { BasketService } from 'src/app/basket/basket.service';
import { IProduct } from 'src/app/shared/Models/product';
#Component({
selector: 'app-product-item',
templateUrl: './product-item.component.html',
styleUrls: ['./product-item.component.scss'],
})
export class ProductItemComponent implements OnInit {
#Input() product: IProduct;
constructor(private basketService: BasketService) {}
ngOnInit(): void {}
addItemToBasket() {
this.basketService.addItemToBasket(this.product);
}
}
product-item.html
<div class="card h-100 shadow-sm">
<div class="image position-relative" style="cursor: pointer">
<img
src="{{ product.pictureUrl }}"
alt="{{ product.name }}"
srcset=""
class="img-fluid bg-light"
/>
<div class="d-flex align-items-center justify-content-center hover-overlay">
<button
(click)="addItemToBasket()"
type="button"
class="btn btn-secondary fa fa-shopping-cart me-2"
></button>
<button
routerLink="/shop/{{ product.id }}"
type="button"
class="btn btn-secondary"
>
view
</button>
</div>
</div>
<div class="card-body d-flex flex-column">
<a routerLink="/shop/{{ product.id }}">
<h6 class="text-uppercase">{{ product.name }}</h6>
</a>
<span class="mb-2">{{ product.price | currency }}</span>
</div>
</div>
so basically the problem appears when i click on a product the id of of the basket is always null
the items are adding up but the Id is null
ex:{
"id": "null",
"items": [
{
"id": 8788,
"productName": "T Blues",
"price": 20,
"pictureUrl": "*********/images/products/bo.png",
"quantity": 1,
"brand": "Y",
"type": "Z"
}
]
}
any idea how to solve this ?
The BehaviorSubject returns the last item sent to it.
you are initializing it with a null:
(...)
export class BasketService {
baseUrl = environment.apiUrl;
// \/ here
private basketSource = new BehaviorSubject<IBasket>(null);
so the first time you subscribe to its value, it will yield null
I don't see a call to getBasket on your component, it might not have been called... or the service might not have responded in time to provide the next value into basketSource
also BehaviorSubject's are better used as Observables, to avoid issues with its value.
you should remove the function
getCurrentBasketValue() {
return this.basketSource.getValue();
}
make the prop basketSource public and get its value filtering out nulls when needed as the following:
basketService.basketSource.pipe(filter(x=>x), take(1)).subscribe(source => {
// your code here,
})
this problem is driving me crazy.
I have an array defined within a service, which is used in 3 other components:
This is the service, file products.service.ts (notice the product array of Products)
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Product } from './../models/Product';
import { ProductForm, productFormToProduct } from './../models/ProductForm';
// #Injectable({
// providedIn: 'root'
// })
const apiUrl = 'http://localhost:3000/products';
#Injectable()
export class ProductsService {
public products: Product[] = [];
constructor(private http: HttpClient) {}
getProducts() {
return this.http.get(apiUrl)
}
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
return this.http.delete(apiUrl + "/" + p.id)
}
storeNewProduct(pf: ProductForm) {
const idList = this.products.map((x) => {return x.id});
const i = Math.max(...idList) + 1;
const p = productFormToProduct(pf);
p.id = i;
this.products.push(p);
return this.http.post(apiUrl, p)
}
}
This is the component where i subscribe to getProducts, and fill the array (file products.component.ts):
import { Component, OnInit } from '#angular/core';
import { ProductsService } from '../../shared/services/products.service';
import { Product } from '../../shared/models/Product';
#Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
}
}
And this is the component where i subscribe to deleteProduct (file product-card.component.ts):
import { Component, Input, OnInit } from '#angular/core';
import { ProductsService } from '../../services/products.service';
import { Product } from './../../models/Product';
#Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss']
})
export class ProductCardComponent implements OnInit {
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
}
#Input() product: Product
public buttonDeleteFunction() {
this.productsService.deleteProduct(this.product).subscribe();
}
}
The problem is, when i click on some delete product button, i have this weird behaviour:
Before click:
After click:
Here is the products.component.html file:
<div class="products__header">
<h3 class="products__heading">
Listado de productos ({{ products.length }})
</h3>
<input
class="products__search"
placeholder="Buscador"
type="search"
[(ngModel)]="searchText"
/>
</div>
<p *ngFor="let p of products">{{ p.name }}</p>
<p>{{ products }}</p>
<div class="products__list">
<app-product-card
*ngFor="let p of products | filterNames: searchText"
[product]="p"
></app-product-card>
</div>
Why do i get the expected behaviour in only two of the four places where i use the products list?
I know i can use an Output to manually remove the item from the list when i click the button, but i have been told that services are used instead of Inputs/Outputs when i want to share between multiple components, so i'd rather not use an Output for this
When you use your approach with common data on service layer then a common pitfall is that Angular does not detect the changes that affect your component. In that case you must inform your component for those changes using an emmiter.
Use an emmiter on service
productUpdated :EventEmitter = new EventEmitter();
deleteProduct(p: Product) {
// this.products = this.products.filter(prod => prod.id !== p.id);
const i = this.products.indexOf(p);
this.products.splice(i,1);
this.productUpdated.emit(this.products);
return this.http.delete(apiUrl + "/" + p.id)
}
And then listen for that change ProductsComponent
export class ProductsComponent implements OnInit {
products: Product[] = [];
searchText: string = "";
constructor(private productsService: ProductsService) {}
ngOnInit(): void {
this.productsService.getProducts()
.subscribe((data: Product[]) => {
this.productsService.products = data;
this.products = this.productsService.products;
})
this.productsService.productUpdated.subscribe( (data) => {
this.products = data;
});
}
I am implementing a simple Fruit Shop web application using Angular in which I am experiencing a strange exception on which I have spent lots of time, but have NOT found any solution yet.
The overall application's scenario is as follows:
In the root route (page) I am loading a bunch of fruits (each fruit is shown like Bootstrap Card), among which the end-user can choose and add them to his/her Cart (through Add to Card, - and + buttons); and then he/she can go to his/her Cart to checkout and payment process.
My important implemented components are as follows:
products (which is loaded as main component in app component and plays the main page role of the application)
productCard (which plays the Bootstrap Card role and is loaded in products component using an *ngFor
productQuantity (which provides the Add to Card, - and + buttons inside the productCard component. I have implemented this feature as a component, as I want to use this feature in the Cart page, allowing the end-use to add or subtract his/her choices there, too)
I have also implemented some simple models to play the entity roles as follows:
cart.ts (The most important properties and methods of this class are CartItem[] array to persist the items chosen by the end-user inside his/her cart, and also getQuantity(product: Product) method that gets a product and returns a number which refers to the total number of that product inside the end-user's cart at the time)
cart-item.ts (The most important properties of this class are Quantity and Product properties)
product.ts (The most important properties of this class are Name and Fee properties.
Here are the codes be more clear for you:
Products Component Class:
import { Component, OnInit, OnDestroy } from '#angular/core';
import { ProductService } from '../product.service';
import { Product } from '../models/product';
import { Subscription } from 'rxjs';
import { AuthService } from '../auth.service';
import { Cart } from '../models/cart';
import { ShoppingCartService } from '../shopping-cart.service';
#Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
products: Product[];
filteredProducts: Product[];
productsSubscription: Subscription;
cartSubscription: Subscription;
cart: Cart;
constructor(
private productService: ProductService,
public auth: AuthService,
private cartService: ShoppingCartService) {
}
filter(query: string) {
this.filteredProducts = (query) ?
this.products.filter(p => p.Name.toLowerCase().includes(query.toLowerCase())) :
this.products;
}
async ngOnInit() {
if (this.auth.isLoggedIn()) {
(await this.cartService.getCart())
.subscribe(cart => {
this.cart = cart;
this.productService.getAll().then(v =>
v.subscribe(products => {
this.filteredProducts = this.products = products
}));
});
}
}
}
Products Component Markup:
<h4>Welcome to Fruit Shop. <ng-container *ngIf="!auth.isLoggedIn()"> To purchase fruits you must <span>log in first</span>.</ng-container></h4>
<p>
<input type="text" #query (keyup)="filter(query.value)"
placeholder="Type here to search among products..." class="form-control">
</p>
<div class="row">
<ng-container *ngFor="let p of filteredProducts">
<div class="col">
<product-card [product]="p" [cart]="cart"></product-card>
</div>
</ng-container>
</div>
ProductCard Component Class:
import { Component, OnInit, Input } from '#angular/core';
import { Product } from '../models/product';
import { ShoppingCartService } from '../shopping-cart.service';
import { AuthService } from '../auth.service';
import { Cart } from '../models/cart';
#Component({
selector: 'product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.css']
})
export class ProductCardComponent {
#Input('product') product: Product;
#Input('cart') cart: Cart;
constructor(public cartService: ShoppingCartService, public auth: AuthService) {
}
async addToCart() {
this.cartService.addToCart(this.product).subscribe(
res => this.cartService.getCart().then(
res => {
res.subscribe(c => this.cart = c)
}).then(value => this.cartService.totalCartItems += 1)
);
}
}
ProductCard Component Markup:
<div class="card" style="width: 15rem;">
<img class="card-img-top" [src]="product.imageUrl" alt="Card image cap">
<div class="card-body">
<h5 class="card-title">{{ product.Name }}</h5>
<span class="card-text">{{ product.Fee | currency }}</span>
<button
*ngIf="cart && cart.getQuantity(product) > 0"
(click)="cartService.deleteFromCart(product)"
type="button" class="btn btn-danger btn-sm">Delete</button>
</div>
<div class="card-footer">
<button *ngIf="!auth.isLoggedIn() || (cart && cart.getQuantity(product)) === 0; else updateQuantity"
[ngClass]="!auth.isLoggedIn() ? 'button-not-allowd' : 'button-cursor'"
[disabled]="!auth.isLoggedIn()"
(click)="addToCart() " class="btn btn-primary btn-block ">Add to Cart</button>
<ng-template #updateQuantity>
<product-quantity [product]="product" [cart]="cart"></product-quantity>
</ng-template>
</div>
</div>
ProductQuantity Component Class:
import { Component, OnInit, Input } from '#angular/core';
import { Product } from '../models/product';
import { Cart } from '../models/cart';
import { ShoppingCartService } from '../shopping-cart.service';
#Component({
selector: 'product-quantity',
templateUrl: './product-quantity.component.html',
styleUrls: ['./product-quantity.component.css']
})
export class ProductQuantityComponent {
#Input('product') product: Product;
#Input('cart') cart: Cart;
constructor(private cartService: ShoppingCartService) {
}
async addToCart() {
this.cartService.addToCart(this.product).subscribe(
res => this.cartService.getCart().then(
res => {
res.subscribe(c => this.cart = c)
}).then(value => this.cartService.totalCartItems += 1)
);
}
subtractFromCart() {
this.cartService.subtractFromCart(this.product).subscribe(
res => this.cartService.getCart().then(
res => res.subscribe(c => this.cart = c)).then(value => this.cartService.totalCartItems -= 1)
);
}
deleteFromCart() {
this.cartService.deleteFromCart(this.product).subscribe(
res => this.cartService.getCart().then(
res => res.subscribe(c => this.cart = c))
.then(value => this.cartService.totalCartItems -= 1)
);
}
}
ProdcutQuantity Component Markup:
<div class="row no-gutters">
<div class="col-2 ">
<button (click)="cart.getQuantity(product) > 1 ? subtractFromCart() : deleteFromCart()"
class="btn btn-secondary btn-block ">-</button>
</div>
<div class="col text-center quantity-text">
{{ cart.getQuantity(product) }} in cart
</div>
<div class="col-2 ">
<button (click)="addToCart()" class="btn btn-secondary btn-block ">+</button>
</div>
</div>
Cart Service which is a service and is used to communicate to the server side of the application about cart related things:
import { Injectable } from '#angular/core';
import { Product } from './models/product';
import { AuthService } from './auth.service';
import { Router } from '#angular/router';
import { Cart } from './models/cart';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Observable, of } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class ShoppingCartService {
totalCartItems: number = 0;
constructor(private auth: AuthService, private router: Router, private http: HttpClient) {
if (auth.isLoggedIn()) {
this.getCart().then(resp =>
resp.subscribe(cart => cart.CartItems.forEach(item => this.totalCartItems += item.Quantity)))
}
}
addToCart(product: Product) {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
})
};
return this.http.post(
`http://localhost/FruitShop/api/v1/CartItems/AddItemByItemId/?productId=${product.Id}`,
{},
httpOptions);
}
subtractFromCart(product: Product) {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
})
};
return this.http.post(
`http://localhost/FruitShop/api/v1/CartItems/SubstractItemByItemId/?productId=${product.Id}`,
{},
httpOptions);
}
deleteFromCart(product: Product) {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
})
};
return this.http.post(
`http://localhost/FruitShop/api/v1/CartItems/DeleteItemByItemId/?productId=${product.Id}`,
{},
httpOptions);
}
async getCart(): Promise<Observable<Cart>> {
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
})
};
return this.http.get<Cart>('http://localhost/FruitShop/api/v1/Cart/GetCart', httpOptions);
}
}
cart.ts which is my cart model:
import { CartItem } from './cart-item';
import { Product } from './product';
export class Cart {
Id: number;
IsActive: Boolean;
IsDeleted: Boolean;
CreatedAt: Date;
User_Id: number;
Coupons_Id: number;
CartItems: CartItem[];
getQuantity(product: Product): number {
let count: number = 0;
this.CartItems
.filter(ci => ci.Products_Id == product.Id)
.forEach(i => {
count += i.Quantity;
});
return count;
}
}
cart-item.ts which is my CartItem model:
import { Product } from './product';
export class CartItem {
constructor(
public Id: number,
public Quantity: number,
public ModifiedAt: Date,
public IsDeleted: Boolean,
public Products_Id: number,
public Carts_Id: number,
public Products: Product
)
{}
get totalPrice() {
return this.Quantity * this.Products.Fee;
}
}
product.ts which is my Product model:
export class Product {
constructor(public Id: number, public Name: string, public Fee: number, public imageUrl: string){
}
}
Here is the exception that I don't know how to solve it, after searching the web and google a lot:
core.js:6185 ERROR TypeError: ctx.cart.getQuantity is not a function
at ProductCardComponent_Template (product-card.component.html:7)
at executeTemplate (core.js:11949)
at refreshView (core.js:11796)
at refreshComponent (core.js:13229)
at refreshChildComponents (core.js:11527)
at refreshView (core.js:11848)
at refreshDynamicEmbeddedViews (core.js:13154)
at refreshView (core.js:11819)
at refreshComponent (core.js:13229)
at refreshChildComponents (core.js:11527)
I guess the problem is that cart isn't instantly available and comes from a subscription in Products Component Class. It means at the beginning in a moment of time it is undefined and getQuantity isn't a function.
Try the next thing to solve it: add ngIf on the div.row
Products Component Markup:
<h4>Welcome to Fruit Shop. <ng-container *ngIf="!auth.isLoggedIn()"> To purchase fruits you must <span>log in first</span>.</ng-container></h4>
<p>
<input type="text" #query (keyup)="filter(query.value)"
placeholder="Type here to search among products..." class="form-control">
</p>
<div class="row" *ngIf="cart">
<ng-container *ngFor="let p of filteredProducts">
<div class="col">
<product-card [product]="p" [cart]="cart"></product-card>
</div>
</ng-container>
</div>
now the product cart will be rendered only when the cart exists that shouldn't cause the error.
I have three components. A parent component (dashboard.component.ts) which houses two siblings (poll-form.component.ts and poll.component.ts). One of the siblings, poll-component sends an Output() back to the parent called (editEvent)="editPoll($event)". When this Output() is sent I'd like that to trigger a function inside of the other sibling poll-form.component instead of triggering the function in the parent dashboard.component. What's the correct way of doing this?
My current implementation "sort of" works. It fires the function in the parent and then the parent imports the component and calls the function in the sibling component. The issue, however, is that sibling component skips the ngOnInit() and doesn't have access to the component variables needed within the function.
Ultimately what I'm trying to do is break out the form into its own component but the act of opening the form is triggered by a sibling component.
dashboard.component.html
<div class="page">
<div style="margin-bottom: 20px; margin-right: 20px; text-align: left;">
<button type="button" (click)="showCreateModal()" pButton icon="pi pi-check" label="Create Poll"></button>
</div>
<div *ngFor="let pollId of pollIds" style="display: inline-block;">
<app-poll [pollKey]="pollId" (editEvent)="editPoll($event)" (deleteEvent)="deletePoll($event)"></app-poll>
</div>
</div>
<div *ngIf="displayCreateModal">
<app-poll-form [type]="'create'" (closeEvent)="closeModal($event)"></app-poll-form>
</div>
<div *ngIf="displayEditModal">
<app-poll-form [type]="'edit'" (closeEvent)="closeModal($event)"></app-poll-form>
</div>
poll.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { first } from 'rxjs/operators';
import { CardModule } from 'primeng/card';
import { AngularFireAuth } from '#angular/fire/auth';
#Component({
selector: 'app-poll',
templateUrl: './poll.component.html',
styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
poll:any;
#Input()
pollKey: string;
#Output()
editEvent = new EventEmitter<string>();
constructor(private firebaseService: FirebaseService, private afAuth: AngularFireAuth) { }
ngOnInit() {
this.firebaseService.getPoll(this.pollKey).subscribe(pollDoc => {
if (!pollDoc.payload.exists) {
return;
}
const pollData:any = pollDoc.payload.data();
this.poll = {
id: pollDoc.payload.id,
helperText: pollData.helperText,
pollType: pollData.pollType,
scoringType: pollData.scoringType,
user: pollData.user
};
}
edit() {
this.editEvent.emit(this.poll);
}
}
poll-form.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import { DialogModule } from 'primeng/dialog';
import { DropdownModule } from 'primeng/dropdown';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { SelectButtonModule } from 'primeng/selectbutton';
import { FirebaseService } from '../services/firebase.service';
import nflPlayers from '../../assets/data/players-nfl.json';
import nflPollTypes from '../../assets/types/poll-types-nfl.json';
import nflScoringTypes from '../../assets/types/scoring-types-nfl.json';
#Component({
selector: 'app-poll-form',
templateUrl: './poll-form.component.html',
styleUrls: ['./poll-form.component.scss']
})
export class PollFormComponent implements OnInit {
title:string;
btnLabel:string;
selectedScoringType:any;
choices:any;
selectedPollType:any;
selectedPollKey:any;
displayEditModal:boolean = false;
displayCreateModal:boolean = false;
filteredPlayersMultiple: any[];
displayModal:boolean = true;
nflPlayers:any = nflPlayers.Players;
nflPollTypes:any = nflPollTypes.types;
nflScoringTypes:any = nflScoringTypes.types;
activePlayers:any;
#Input()
type: string;
#Output()
closeEvent = new EventEmitter<string>();
constructor(
private firebaseService: FirebaseService
) { }
ngOnInit() {
this.initFormDefaults();
}
initFormDefaults() {
this.choices = [[],[]];
this.selectedScoringType = this.nflScoringTypes[0];
this.selectedPollType = this.nflPollTypes[0];
if (this.type == "create") {
this.btnLabel = "Create";
this.title = "Create Poll";
} else {
this.btnLabel = "Update";
this.title = "Edit Poll";
}
// Filter out active NFL players
this.activePlayers = this.nflPlayers.filter(player => player.active == "1");
}
editPoll(poll) {
this.type = "edit"
this.activePlayers = this.nflPlayers.filter(player => player.active == "1");
this.selectedPollKey = poll.id;
// Set scoring type
this.selectedScoringType = this.nflScoringTypes.find((type) => {
return type.code == poll.scoringType
});
// Set poll type
this.selectedPollType = this.nflPollTypes.find((type) => {
return type.code == poll.pollType
});
// Populate edit modal with properly formatted Player objects
for (let i=0; i < poll.choices.length; i++) {
this.choices[i] = poll.choices[i].players.map((choicePlayer:any) => {
return this.activePlayers.find(player => player.playerId == choicePlayer.id);
});
}
}
}
dashboard.component.ts
import { Component, OnInit } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/auth';
import { first } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { PollFormComponent } from '../poll-form/poll-form.component';
#Component({
providers: [PollFormComponent],
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
displayEditModal:boolean = false;
constructor(
private pollFormComponent: PollFormComponent
) { }
ngOnInit() {
}
editPoll(poll) {
this.displayEditModal = true;
this.pollFormComponent.editPoll(poll);
}
}
I have component where I implement ControlValueAccessor and I'm having problems understanding the correct way to use it:
import { Component, OnInit, forwardRef, Output, EventEmitter, OnChanges, Input, ViewChild } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
import { UserOrEmail } from '../entities/UserOrEmail';
export const USER_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
// tslint:disable-next-line
useExisting: forwardRef(() => AddUserOrEmailComponent),
multi: true,
};
const noop = () => {
// Placeholder operation
};
#Component({
selector: 'app-add-user-or-email',
templateUrl: './add-user-or-email.component.html',
providers: [USER_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class AddUserOrEmailComponent implements OnInit, ControlValueAccessor {
#Input()
user: any = UserOrEmail;
#Output()
change: EventEmitter<UserOrEmail> = new EventEmitter<UserOrEmail>();
users: any = [];
ngOnInit() {
this.user = {
userId: 'ull',
name: 'null',
email: 'null'
};
this.users = ConstantService.UserArray;
}
// #region [ Value Accessor Interface ]--------------------------------------------------------
// Placeholders for the callbacks which are later provided
// by the Control Value Accessor
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
get value(): any {
return this.user;
}
// [ ControlValueAccessor interface implementation ]-------------------------------------------
set value(v: any) {
if (this.user !== v) {
this.user = <UserOrEmail>v;
this.onChangeCallback(v);
this.change.next(this.user);
}
}
writeValue(value: any) {
if (value !== this.user)
this.user = <UserOrEmail>value;
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
and html:
<div>
<div class="form-column">
<div class="form-row">
<label>
{{'GENERIC.USER'|translate}}
</label>
<select>
<option [ngValue]="'default'"></option>
<option *ngFor="let user of users" [ngValue]="user">{{user.login}}</option>
</select>
</div>
<div class="form-row">
<label for="addPersonEmail" >{{'GENERIC.EMAIL' | translate}}</label>
<input type="email" placeholder="{{'GENERIC.EMAIL'|translate}}" pattern="^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$">
</div>
</div>
</div>
I try to use it in another component:
hmtl:
<app-modal class="form" #addMilestoneModal [width]="550">
<div header>{{'MODALS.ADD_MILESTONE'|translate}}</div>
<div body>
<div class="form-column">
<div class="form-value">
<app-add-user-or-email #addUser [(ngModel)]="milestone.assignee"></app-add-user-or-email>
</div>
<div class="form-row">
<label for="addMilestoneDescription">{{'GENERIC.DESCRIPTION' | translate}}</label>
<textarea style="height: 150px" [(ngModel)]="milestone.description"></textarea>
</div>
</div>
</div>
<div footer class="flex-container">
<button class="flex-item-row btn btn-a" [disabled]="!milestone.description" (click)="apply()">{{'GENERIC.APPLY'
| translate}}</button>
</div>
</app-modal>
Typescript:
import { Component, ViewChild, EventEmitter } from '#angular/core';
import { ModalComponent } from '../../widgets/modal/modal.component';
import { Milestone } from '../../entities/Milestone';
import { ConstantService } from '../../services/ConstantService';
import { AddUserOrEmailComponent } from '../../add-user-or-email/add-user-or-email.component';
#Component({
selector: 'app-add-milestone-modal',
templateUrl: './add-milestone-modal.component.html'
})
export class AddMilestoneModalComponent {
#ViewChild('addMilestoneModal')
modal: ModalComponent;
cs = ConstantService;
emitter: EventEmitter<Milestone> = new EventEmitter<Milestone>();
milestone: any = Milestone;
apply() {
console.log(this.milestone); // <---- HERE IT SHOULD BE ACCESSED
debugger;
this.emitter.next(this.milestone);
this.modal.close();
}
cancel() {
this.emitter.next(null);
this.modal.close();
}
}
I should get it in milestone object but it is empty. What am I missing?
I had problems with this a couple of weeks ago and made a StackBlitz with a good example.
Hopefully this can help you?
https://stackblitz.com/edit/mat-select-with-controlvalueaccessor