I am creating a shopping cart in Angular 4 and want to check if a new product prod yet exists in the cartProducts array.
Here's my Component:
Component
import { Component, OnInit } from '#angular/core';
import { Router } from "#angular/router";
import { ProductsService } from '../service/products.service';
#Component({
selector: 'app-store',
templateUrl: './store.component.html',
styleUrls: ['./store.component.css']
})
export class StoreComponent implements OnInit {
itemCount: number;
cartProducts: any = [];
productsList = [];
constructor( private _products: ProductsService ) { }
ngOnInit() {
this.itemCount = this.cartProducts.length;
this._products.product.subscribe(res => this.cartProducts = res);
this._products.updateProducts(this.cartProducts);
this._products.getProducts().subscribe(data => this.productsList = data);
}
addToCart(prod){
this.cartProducts.hasOwnProperty(prod.id) ? console.log("Added yet!") : this.cartProducts.push(prod);
console.log(this.cartProducts)
}
}
My addToCart function which is fired by click works fine, but only from second time.
1 click - we add a product in the empty cartProducts array, the product is added
2 click - although the product is added, it is added again and there are two same products in the array now. I've got the array with the two same products.
3 click - console shows "Added yet!", now it recognizes that the product is in the array yet.
UPD
The product is an object of type:
{
"id" : "1",
"title" : "Title 1",
"color" : "white"
}
How to fix the issue?
hasOwnProperty is for checking if a key exists in an object, you're using it for an array. Use this instead:
addToCart(prod){
this.cartProducts.indexOf(prod) > -1 ? console.log("Added yet!") : this.cartProducts.push(prod);
console.log(this.cartProducts)
}
try this :
let idx = this.cartProducts.findIndex(elem => {
return prod === elem
})
if (idx !== -1) {
console.log("Added yet!")
} else {
this.cartProducts.push(prod);
}
Related
I have this array of recipes where each object is a specific recipe and each recipe has an array of ingredients and each ingredient it's an object made of _id name quantity. My problem added below if you guys have any idea why is this happening please let me know. I am struggling for 3 days...any advice would be really appreciated. Thanks a lot!
(3) [{…}, {…}, {…}]
0:
ingredients: Array(4)
0: {_id: "5f6628d0029e87e02c79ce0a", name: "chia", quantity: 10}
1: {_id: "5f6628d0029e87e02c79ce0b", name: "apple", quantity: 15}
2: {_id: "5f6628d0029e87e02c79ce0c", name: "honey", quantity: 30}
3: {_id: "5f6628d0029e87e02c79ce0d", name: "almond flour", quantity: 35}
length: 4
__proto__: Array(0)
name: "Coconut Chia Pudding"
__v: 0
_id: "5f6628d0029e87e02c79ce09"
__proto__: Object
1: {_id: "5f6628d0029e87e02c79ce0e", name: "Peanut Butter Cookies", ingredients: Array(4), __v: 0}
2: {_id: "5f6628d0029e87e02c79ce13", name: "Caprese Avocado Bowls", ingredients: Array(3), __v: 0}
length: 3
__proto__: Array(0)
What I have in UI it's a list with the above recipes which a user can tick and untick and after a recipe has been ticked its ingredients are showed into a list.
HTML
<ion-content>
<ion-grid>
<ion-row>
<ion-col>
<ion-list>
<ion-item
*ngFor="let recipe of loadedRecipes; let lastRecipe = last"
[ngClass]="{ 'last-recipe': lastRecipe }"
>
<ion-checkbox
(ionChange)="onCheckRecipe($event)"
value="{{recipe.name}}"
></ion-checkbox>
<ion-label>{{recipe.name}}</ion-label>
<ion-button
[routerLink]="['/','recipes','recipe-details', recipe._id]"
>></ion-button
>
</ion-item>
</ion-list>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<h6 class="ion-padding-start" *ngIf="groceryList.length > 0">
Grocery List
</h6>
<ion-list *ngIf="groceryList.length > 0">
<ion-item *ngFor="let ingredient of groceryList">
<ion-label>{{ingredient.name}}</ion-label>
<ion-note slot="end">{{ingredient.quantity}} g</ion-note>
</ion-item>
</ion-list>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
What I want to achieve (and I've done it below, but I have a bug) is when the user ticks a recipe its ingredients to be added into an array called groceryList and when unticks the recipe to remove the ingredients from my groceryList array. Also, if I have ticked 1 recipe and if the next one I am going to tick has the same ingredient as the one ticked before, just to increment that common ingredient quantity that already exists, and NOT to add it twice and if I want to untick a recipe to remove the uncommon ingredients and subtract the quantity of the common ingredient. I already managed to do it, BUT I have a big problem and I don't know where is coming from. at some point in UI if I tick and untick recipes and I tick the ones that have the same ingredient one after another it removes the common ingredient even though I still have a ticked recipe that has that ingredient. Again, if you guys have any idea why is this happening please let me know I would appreciate any advice you have
My TS
import { Component, OnInit } from "#angular/core";
import { Subscription } from "rxjs";
import { RecipesService } from "src/app/services/recipes.service";
#Component({
selector: "app-recipes",
templateUrl: "./recipes.page.html",
styleUrls: ["./recipes.page.scss"],
})
export class RecipesPage implements OnInit {
loadedRecipes: any;
private _recipesSub: Subscription;
constructor(private recipesService: RecipesService) {}
groceryList = [];
ngOnInit() {
this._recipesSub = this.recipesService.recipes.subscribe((receivedData) => {
this.loadedRecipes = receivedData;
});
}
onCheckRecipe(e) {
if (e.detail.checked === true) {
for (let recipe of this.loadedRecipes) {
console.log(this.loadedRecipes);
if (recipe.name === e.detail.value) {
for (let eachIngredient of recipe.ingredients) {
let matchedIng = this.groceryList.find(function (foundIng) {
return foundIng.name === eachIngredient.name;
});
if (matchedIng) {
matchedIng.quantity =
matchedIng.quantity + eachIngredient.quantity;
} else {
this.groceryList.push(eachIngredient);
}
}
}
}
} else {
for (let recipe of this.loadedRecipes) {
if (recipe.name === e.detail.value) {
for (let eachIngredient of recipe.ingredients) {
let matched = this.groceryList.find(function (foundIngre) {
return foundIngre.name === eachIngredient.name;
});
if (
matched.name === eachIngredient.name &&
matched._id === eachIngredient._id
) {
let index = this.groceryList.findIndex(
(x) => x._id === matched._id
);
this.groceryList.splice(index, 1);
} else {
matched.quantity = matched.quantity - eachIngredient.quantity;
}
}
}
}
}
}
ionViewWillEnter() {
this.recipesService.fetchRecipes().subscribe();
}
ngOnDestroy() {
if (this._recipesSub) this._recipesSub.unsubscribe();
}
}
The problem lies in the flow of your if statements. In the "onRemove" section of your code, you are saying "If the ingredient is in the list, remove it from the list. If not, decrement its quantity." That second part doesn't make any sense and more importantly, you'll never reach it because the ingredient should always be in the list.
for (let eachIngredient of recipe.ingredients) {
let matched = this.groceryList.find(function(foundIngre) {
return foundIngre.name === eachIngredient.name;
});
if (
matched.name === eachIngredient.name &&
matched._id === eachIngredient._id
) {
let index = this.groceryList.findIndex(
(x) => x._id === matched._id
);
// Problem e ca eachIngredient.quantity se schimba
this.groceryList.splice(index, 1);
} else {
matched.quantity = matched.quantity - eachIngredient.quantity;
}
}
Based on what you've said, what you want to do is:
subtract the quantity which was attributed to the removed recipe
if the new quantity is zero, remove the ingredient from the list (though you could also leave it and ignore ingredients with zero quantity)
Try this instead:
for (let eachIngredient of recipe.ingredients) {
// I am assuming that ids are unique so I am not checking foundIngre.name at all,
// since I assume that ingredients with the same name must also have the same name
// I am also using findIndex first so that you don't need a second find when removing
const matchIndex = this.groceryList.findIndex(
(foundIngre) => foundIngre._id === eachIngredient._id
);
if ( matchIndex ) { // this should always be true
const matched = this.groceryList[matchIndex];
// preserve the entry if there is still some quantity
if ( matched.quantity > eachIngredient.quantity ) {
matched.quantity = matched.quantity - eachIngredient.quantity; // can use -= to shorten
}
// remove from the list only if there is no quantity remaining
else {
this.groceryList.splice(matchIndex, 1);
}
}
}
EDIT:
Trying to update and remove items in an array is an unnecessary pain. The reworked version of your code stores the _groceryList in a keyed dictionary instead. I initially intended to key by ingredient id, but after reviewing your demo I see that I was incorrect in my assumption that the same ingredient in multiple recipes would share the same id. So instead I am keying by ingredient name. This way you can write to _groceryList[name] and it doesn't matter whether it previously existed or not.
The class has a public getter groceryList which converts the private _groceryList dictionary into an array.
I've also tried to remove unnecessary code duplication across scenario branches by using a generic toggleIngredient function which uses the boolean checked to control whether it is adding or subtracting by multiplying by plus or minus one.
import { Component } from "#angular/core";
import { Platform } from "#ionic/angular";
import { SplashScreen } from "#ionic-native/splash-screen/ngx";
import { StatusBar } from "#ionic-native/status-bar/ngx";
import { Subscription } from "rxjs";
export interface Ingredient {
_id: string;
name: string;
quantity: number;
}
export interface Recipe {
_id: string;
name: string;
ingredients: Ingredient[];
}
#Component({
selector: "app-root",
templateUrl: "app.component.html"
})
export class AppComponent {
private _recipesSub: Subscription;
constructor(
private platform: Platform,
private splashScreen: SplashScreen,
private statusBar: StatusBar,
) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
this.statusBar.styleDefault();
this.splashScreen.hide();
});
}
private loadedRecipes: Recipe[] = [/*...*/]
// store the groceryList in a dictionary keyed by name
private _groceryList: Record<string, Ingredient> = {};
// getter returns the groceryList in array format, ignoring 0 quantities
get groceryList(): Ingredient[] {
return Object.values(this._groceryList).filter( ing => ing.quantity > 0 );
}
// get the current quantity for an ingredient by name, or 0 if not listed
currentQuantity( name: string ): number {
const ingredient = this._groceryList[name];
return ingredient ? ingredient.quantity : 0;
}
// update the quantity for an ingredient when checked or unchecked
// will add new ingredients, but never removes old ones
toggleIngredient( ingredient: Ingredient, checked: boolean ): void {
// add to or remove from quantity depending on the value of checked
const quantity = this.currentQuantity(ingredient.name) + (checked ? 1 : -1 ) * ingredient.quantity;
// replace the object in the grocery list dictionary
this._groceryList[ingredient.name] = {
...ingredient,
quantity
}
}
onCheckRecipe(e) { // you'll want to add a type for e here
for (let recipe of this.loadedRecipes) {
// find the matching recipe
if (recipe.name === e.detail.value) {
// loop through the recipe ingredients
for (let eachIngredient of recipe.ingredients) {
this.toggleIngredient(eachIngredient, e.detail.checked)
}
}
}
}
}
I think the issue seems to be with this section of the code
if (
matched.name === eachIngredient.name &&
matched._id === eachIngredient._id
) {
let index = this.groceryList.findIndex(
(x) => x._id === matched._id
);
// Problem e ca eachIngredient.quantity se schimba
this.groceryList.splice(index, 1);
} else {
matched.quantity = matched.quantity - eachIngredient.quantity;
}
The if statement should be checking on the quanity instead of validating the name and id again , something like
if(matched.quantity <= eachIngredient.quantity){
// splice the item and remove it.
}
else
{
// decrease the quantity
}
A small suggestion to finding the matched ingredient. First use findIndex() to retrieve matchedIndex, and use grocerylist[matchedIndex] to retrieve the item, to avoid iterating through the grocerylist again to find the index for splicing.
I have a parent component (B) that is getting data from it's parent input (A)
(C) have is (B) child component.
Inside (B) I'm having a selector that gets data from the store.
export class BComponent implements OnChanges {
#Input() branchId;
ngOnChanges() {
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
directionChanged(event) {
this.selectedDirection = event;
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
}
}
directionChanged is the Output event that I get from (C)
The issue this that selectedDataByBranch subscription is not getting the new data update triggered inside selectedDataByBranch$
I have also tried this way
directionChanged(event) {
this.selectedDirection = event;
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection });
}
What i could suggest is. Turn your parameters into a Subject then merge with the store selection, in your directionChanged(event) method provide value to subject.
So your final code will be something like this:
export class BComponent implements OnChanges {
#Input() branchId;
criterias$= new Subject<{branchId:number,dir:number}>;
ngOnChanges() {
this.selectedDataByBranch$ = this.criterias$.pipe(mergeMap(criteria=> this.store.pipe(
select(selectBranchDirections, { branchId: criteria.branchId, dir: this.searchDirection})
)));
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
this.criterias$.next({branchId:this.branchId,dir:this.sortDirection}); // init first call
}
directionChanged(event) {
this.selectedDirection = event;
this.criterias$.next({ branchId: criteria.branchId, dir: this.searchDirection}});
);
}
}
This stackblitz tries to materialize what i say.
I have a list of items along with their associated check-boxes.
And I'd like to achieve the following:
select/un-select all items in list using "Select All" checkbox.
select/un-select individual items in list.
when all items are selected and any of the selected items is clicked, u-nselect "Select All" checkbox.
Most of these steps are working as expected, except, when I :
select all list-items by checking the "Select All" checkbox
un-checking any of the selected items
then again checking "Select All" checkbox.
This causes that any list-item I un-selected before clicking on "Select All" checkbox, remain un-selected.
It looks as if (for some reason) the internal state of the checkbox is not being changed in this case.
Although, when :
all list-items are un-selected and I select any of the list-items
then check the "Select All" checkbox
it correctly selects all list-items. So I'm a little confused as why it's not working in the other case mentioned above.
Note: The main reason , I don't want to store state for every item in the list, is that I will be using this in a table with virtual-scroll. Which fetches data page by page.. So I don't have access to all items data, hence, I only store either items I manually selected or un-selected.
app.component.ts
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
constructor( private cdr: ChangeDetectorRef ) {
this.cdr.markForCheck();
}
public list = [
"item1", "item2", "item3", "item4", "item5"
];
public selected = {
ids: [],
all: false,
all_except_unselected: false
};
public toggleSelectItem( id: number, event: MouseEvent ): void {
if ( this.selected.all === true ) {
this.selected.all = false;
this.selected.all_except_unselected = true;
this.selected.ids = [];
}
if ( this.selected.all_except_unselected === true ){
this.selected.ids = [ ...this.selected.ids, id ];
} else if ( this.selected.all == false && this.selected.all_except_unselected == false ) {
if ( this.selected.ids.indexOf( id ) === -1 ) {
this.selected.ids = [ ...this.selected.ids, id ];
} else {
this.selected.ids = [ ...this.selected.ids].filter( itemId => itemId !== id );
}
}
console.log(this.selected.ids);
}
public isSelected( id: number ): boolean {
if ( this.selected.all === true ) {
console.log(id, 'selected all')
return true;
} else if ( this.selected.all_except_unselected === true ) {
console.log(id, 'selected all except unselected');
return true;
}
console.log(id, this.selected.ids.indexOf( id ) >= 0 ? 'selected' : 'unselected');
return this.selected.ids.indexOf( id ) >= 0;
}
public toggleSelectAll(): void {
if ( this.selected.all == false ) {
this.selected.ids = [];
}
this.selected.all = !this.selected.all;
this.selected.all_except_unselected = false;
console.log('selected all ', this.selected );
}
}
app.component.html
<input type="checkbox" [checked]="selected.all" (change)="toggleSelectAll()"> Select All
<br>
<br>
<div *ngFor="let item of list; let i = index" >
<input type="checkbox" [checked]="isSelected(i)" (change)="toggleSelectItem(i, $event)"> {{ item }}<br>
</div>
Old code link
Solution
The code is really messy. I would not bother with the booleans, you can make the checks on the array on the fly and the checkboxes will update the state correctly.
Here is the updated code, You can make further modifications but you can get the idea of how this works.
https://angular-hvbfai.stackblitz.io
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
constructor(private cdr: ChangeDetectorRef) {
this.cdr.markForCheck();
}
public list = [
"item1", "item2", "item3", "item4", "item5"
];
public selected = {
ids: []
};
public allSelected() {
return this.selected.ids.length === this.list.length;
}
public toggleSelectItem(id: number, event: MouseEvent): void {
if (this.selected.ids.indexOf(id) === -1) {
this.selected.ids = [...this.selected.ids, id];
} else {
this.selected.ids = [...this.selected.ids].filter(itemId => itemId !== id);
}
console.log(this.selected.ids);
}
public isSelected(id: number): boolean {
return this.selected.ids.indexOf(id) >= 0;
}
public toggleSelectAll(): void {
if (this.allSelected()) {
this.selected.ids = [];
} else {
let i = 0;
this.list.forEach(element => {
if (this.selected.ids.indexOf(i) === -1) {
this.selected.ids = [...this.selected.ids, i];
}
i++;
});
}
console.log('selected all ', this.selected);
}
}
HTML :
<input type="checkbox" [checked]="allSelected()" (change)="toggleSelectAll()"> Select All
<br>
<br>
<div *ngFor="let item of list; let i = index" >
<input type="checkbox" [checked]="isSelected(i)" (change)="toggleSelectItem(i, $event)"> {{ item }}<br>
</div>
The app is written using Ionic. I have a shopping cart where i'm adding products. The product is an object with a note and quantity property. After i use the this.store.dispatch(new PushProductAction({ product })); the note property is concatenated with itself, i.e. i'm adding 3 products to cart with a note 'cold', in the cart this product will have the note property equal to 'coldcoldcold'.
app-state.ts
export class AppState {
categories: Category[];
shopcart: ShopCart;
profile: Profile;
userdata: UserData;
}
product-info.ts
constructor(
private events: Events,
public store: Store<AppState>,
}
pushToCart(product: Product) {
product.quantity = product.quantity ? ++product.quantity : 1;
product.withHalf = this.halfProductStatus;
this.store.dispatch(new PushProductAction({ product }));
}
After the product is added :
goToConfirm(event, desktop = false) {
event.preventDefault();
event.stopPropagation();
jQuery.fancybox.close();
jQuery('.fancybox-close-small').trigger('click');
for (let index = 0; index < this.count; index++) {
this.pushToCart(this.product);
}
this.fixDesign.CreateToast('Product has been added to cart successfuly', 'successToast');
}
actions.ts
export class PushProductAction implements Action {
readonly type = ActionTypes.PUSH_CART;
constructor(public payload: { product: Product; half?: boolean }) {
console.log('PushProductAction');
}
}
P.S. I'm a beginner in angular, if you need more code snippets please let me know.
I have a simple demo app which I'm simulating manually insert / fetch data from DB and injecting new components - according to the num entered.
Plunker
So if I click the "manual " button twice :
And if I set "3" in the text and click "fetch from db" - I get the expected delay(simulate db) and then :
This all works as expected.
The "parent" component is :
//src/MainPage.ts
#Component({
selector: 'my-app',
template: `
<button (click)="putInMyHtml()">Insert component manually</button>
<p> # Items to fetch : <input type="text" style='width:40px' [(ngModel)]="dbNumItems" name="dbNumItems"/> <input type='button' value='fetch from db' (click)='fetchItems($event)'/></p>
<div #myDiv>
<template #target></template>
</div>
`
})
export class MainPage {
#ViewChild('target', { read: ViewContainerRef }) target: ViewContainerRef;
dbNumItems: string;
constructor(private cfr: ComponentFactoryResolver) {}
fetchItems(){
var p= new Promise((resolve, reject) => { //simulate db
setTimeout(()=>resolve(this.dbNumItems),2000)
});
p.then(v=>{
for (let i =0;i<v;i++)
{
this.putInMyHtml() ;// inject "v" times
}
})
}
putInMyHtml() {
// this.target.clear();
let compFactory = this.cfr.resolveComponentFactory(TestPage);
this.target.createComponent(compFactory);
}
}
This is the Injected component :
//src/TestPage.ts
#Component({
selector: 'test-component',
template: '<b>Content : Name={{user.Name}} Age={{user.Age}}</b><br/>',
})
export class TestPage {
#Input
User:Person;
}
So where is the problem ?
As you can see , in the injected component I have :
#Input
User:Person;
which means that I want the parent component to pass a Person object to each injection.
In other words :
Question
Looking at the "after db stage" , How can I pass a customized person to each injection ?
p.then(v=>{
for (let i =0;i<v;i++)
{
let p = new Person();
p.Name = "name"+i;
p.Age = i;
this.putInMyHtml() ; //how to I pass `p` ???
}
})
}
Expected output :
NB
I don't want to use ngFor because I don't need to hold an Array at the back end. this is an app which injects new articles periodically.and I will be glad to know if there's a better way of doing it.
You can do it with the instance property of component ref like this:
putInMyHtml(p) {
// this.target.clear();
let compFactory = this.cfr.resolveComponentFactory(TestPage);
let ref = this.target.createComponent(compFactory);
ref.instance.user = p;
}
-Fixed the #Input() binding, syntax was wrong.
-Added a safe-navigation operator (?) for the template to do the null checks for the async input.
Fixed plunker: https://plnkr.co/edit/WgWFZQLxt9RFoZLR46HH?p=preview
use *ngFor and iterate through an array of Person, that way you can use the #Input. You probably want something like
<ul>
<li *ngFor="let person of people">
<test-component [User]=person></test-component>
</li>
</ul>
add people: Person[] to your main component and when you fetch items
p.then(v=>{
for (let i =0;i<v;i++)
{
let p = new Person();
p.Name = "name"+i;
p.Age = i;
people.push(p)
}
})