Search code examples
angulartypescriptionic3typeerrorsplice

Angular Ionic3 TypeScript - "TypeError: Cannot read property 'name' of undefined" after splicing from an Array


I'm very new to all of this so please take it easy on me, I've searched for everything that I can think of searching for but I just can't figure this out.

I am currently building an ionic3 app and have shopping lists set up so that users can add a recipe to a shopping list - which works fine - but I want to tweak it so that if multiple of the same item are added, instead of having two instances of the same item, it updates the first item to show that it has new data. I can send the items through a service and retrieve them on another page, and I can sort them alphabetically, but I can't get the merging function to work.

Thank you very much in advance for your help.

Here is the (hopefully) relevant code:

import { Injectable } from "@angular/core";
import { Storage } from "@ionic/storage";
import { SLIngredient } from "../data/shopping-interface";

@Injectable()
export class ShoppingService {
  storedDay: SLIngredient[] = []
  storedWeek: SLIngredient[]
  userSL: SLIngredient[] = []
  sortedList: SLIngredient[] = []
  mergedItem: SLIngredient;
  mergedList: SLIngredient[] = [];

  constructor(private storage: Storage) {}

  getShoppingDay(day: number) {
    return this.storage.get('shoppingDay/' + day)
      .then(
        (storedDay: SLIngredient[]) => {
          this.storedDay = storedDay != null ? storedDay : [];
          return this.storedDay.slice();
        }
      )
  }

  getShoppingWeek(id: number) {
    return this.storage.get('shoppingWeek/' + id)
      .then(
        (storedWeek: SLIngredient[]) => {
          this.storedWeek = storedWeek != null ? storedWeek : [];
          return this.storedWeek.slice();
        }
      )
  }

  addItem(name: string, amount: number, measurement: string, completed: boolean) {
    this.userSL.push(new SLIngredient(name, amount, measurement, completed));
  }

  addItems(items: SLIngredient[]) {
    this.userSL.push(...items);
    this.sort()
    this.setUserSL();
    console.log(this.userSL)
  }

  getItems() {
    this.getUserSL;
    return this.userSL.slice();
  }

  sort() {
    this.sortedList = this.userSL.sort((itemA: SLIngredient, itemB: SLIngredient) => {
      const a = itemA.name.toLowerCase()
      const b = itemB.name.toLowerCase()

      if (a > b) return 1;
      if (a < b) return -1;
      if (a == b) {
        this.merge(itemA, itemB);
      }

      return 0
    })

    this.userSL = this.sortedList
  }

  merge(itemA: SLIngredient, itemB: SLIngredient) {
    const newAmount = +itemA.amount + +itemB.amount
    const itemC = new SLIngredient(itemA.name, newAmount, itemA.measurement, itemA.completed)
    this.sortedList.splice(this.sortedList.indexOf(itemA), 1, itemC);
    this.sortedList.splice(this.sortedList.indexOf(itemB), 1);
    // this.sortedList = Object.assign(itemA, itemB, itemC)
    // this.mergedList.push(new SLIngredient(merge.name, merge.amount, merge.measurement, merge.completed))
    // this.sortedList.push(this.mergedItem)
  }

  setUserSL() {
    this.storage.set('userSL', this.userSL)
  }

  getUserSL() {
    return this.storage.get('userSL')
      .then(
        (userSL: SLIngredient[]) => {
          this.userSL = userSL != null ? userSL : [];
          return
          this.userSL.slice();
        }
      )
  }
}

The .sort() function is where the function runs and it is the .merge() function that I find the issue with.

Here is the interface:

export class SLIngredient {

constructor(
    public name: string,
    public amount: number,
    public measurement: string,
    public completed: boolean
){}

I really appreciate any sort of help that anyone can give on this as I'm very new to it all.

Thanks again.

EDIT

When I tried calling

    sort(){
    this.userSL.reduce((reduced, element) => {
        const index = reduced.findIndex(r => r.name.toLowerCase() === element.name.toLowerCase());

        if (index === -1) return [...reduced, element];

        reduced[index].amount += element.amount;

        return reduced;
      }, []).sort((a, b) => {
        const nameA = a.name.toLowerCase()
        const nameB = b.name.toLowerCase()

        if (nameA > nameB) return 1;
        if (nameA < nameB) return -1;

        return 0
      })
}

this is the output I get in the log after adding two of the same recipe to the shopping list:

    (9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
    0: {name: "rolled oats", amount: "1", measurement: "cup"}
    1: {name: "milled flax seed", amount: "1-2", measurement: "tbsp"}
    2: {name: "ground cinnamon", amount: "1", measurement: "tsp"}
    3: {name: "non-diary milk of your choice - Coconut is best", amount: "1.5", measurement: "cups"}
    4: {name: "desiccated coconut", amount: "1", measurement: "tbsp"}
    5: {name: "banana", amount: "1", prep: "sliced"}
    6: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: "1", measurement: "handful"}
    7: {name: "raisins (optional)", amount: "1", measurement: "handful"}
    8: {name: "cocoa powder (optional)", amount: "1", measurement: "tsp"}
    length: 9
    __proto__: Array(0)

    (18) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
    0: {name: "rolled oats", amount: "11", measurement: "cup"}
    1: {name: "milled flax seed", amount: "1-21-2", measurement: "tbsp"}
    2: {name: "ground cinnamon", amount: "11", measurement: "tsp"}
    3: {name: "non-diary milk of your choice - Coconut is best", amount: "1.51.5", measurement: "cups"}
    4: {name: "desiccated coconut", amount: "11", measurement: "tbsp"}
    5: {name: "banana", amount: "11", prep: "sliced"}
    6: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: "11", measurement: "handful"}
    7: {name: "raisins (optional)", amount: "11", measurement: "handful"}
    8: {name: "cocoa powder (optional)", amount: "11", measurement: "tsp"}
    9: {name: "rolled oats", amount: "11", measurement: "cup"}
    10: {name: "milled flax seed", amount: "1-21-2", measurement: "tbsp"}
    11: {name: "ground cinnamon", amount: "11", measurement: "tsp"}
    12: {name: "non-diary milk of your choice - Coconut is best", amount: "1.51.5", measurement: "cups"}
    13: {name: "desiccated coconut", amount: "11", measurement: "tbsp"}
    14: {name: "banana", amount: "11", prep: "sliced"}
    15: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: "11", measurement: "handful"}
    16: {name: "raisins (optional)", amount: "11", measurement: "handful"}
    17: {name: "cocoa powder (optional)", amount: "11", measurement: "tsp"}
    length: 18
    __proto__: Array(0)

SECOND EDIT

This is the part I'm still having issues with:

    addItems(items: SLIngredient[]){
    this.userSL.push(...items);
    this.sort()
    this.setUserSL();
    console.log(this.userSL)
}

sort(){
    this.mergedList = this.userSL.reduce((reduced, element) => {
        const index = reduced.findIndex(r => r.name.toLowerCase() === element.name.toLowerCase());

        if (index === -1) return [...reduced, element];

        reduced[index].amount += element.amount;

        return reduced;
      }, []).sort((a, b) => {
        const nameA = a.name.toLowerCase()
        const nameB = b.name.toLowerCase()

        if (nameA > nameB) return 1;
        if (nameA < nameB) return -1;

        return 0
      })
      this.userSL = this.mergedList
}

setUserSL(){
    this.storage.set('userSL', this.userSL)
}

So I call the addItems() function and it adds it to the array but it also merges it straight away, then for some reason it sees that ingredient as having 2 of itself and so when it's added again it doubles instead of just adding one.

The other issue is that it displays the updated value instead of just pushing that to an array that can be displayed elsewhere.

     (9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
     0: {name: "banana/s", amount: 1, prep: "mashed"}
     1: {name: "rolled oats", amount: 1, measurement: "cup"}
     2: {name: "milled flaxseed", amount: 2, measurement: "tbsp"}
     3: {name: "ground cinnamon", amount: 1, measurement: "tsp"}
     4: {name: "non-dairy milk of your choice (I use Almond)", amount: 1.5, measurement: "cup/s"}
     5: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: 1, measurement: "handful/s"}
     6: {name: "raisins (optional)", amount: 1, measurement: "handful/s"}
     7: {name: "cocoa powder (optional)", amount: 1, measurement: "tsp"}
     8: {name: "desiccated coconut (optional)", amount: 2, measurement: "tsp"}

becomes:

(9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
 0: {name: "banana/s", amount: 2, prep: "mashed"}
 1: {name: "cocoa powder (optional)", amount: 2, measurement: "tsp"}
 2: {name: "desiccated coconut (optional)", amount: 4, measurement: "tsp"}
 3: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: 2, measurement: "handful/s"}
 4: {name: "ground cinnamon", amount: 2, measurement: "tsp"}
 5: {name: "milled flaxseed", amount: 4, measurement: "tbsp"}
 6: {name: "non-dairy milk of your choice (I use Almond)", amount: 3, measurement: "cup/s"}
 7: {name: "raisins (optional)", amount: 2, measurement: "handful/s"}
 8: {name: "rolled oats", amount: 2, measurement: "cup"}
 length: 9
 __proto__: Array(0)

becomes

(9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
0: {name: "banana/s", amount: 4, prep: "mashed"}
1: {name: "cocoa powder (optional)", amount: 4, measurement: "tsp"}
2: {name: "desiccated coconut (optional)", amount: 8, measurement: "tsp"}
3: {name: "frozen mixed berries or fresh seasonal berries (optional)", amount: 4, measurement: "handful/s"}
4: {name: "ground cinnamon", amount: 4, measurement: "tsp"}
5: {name: "milled flaxseed", amount: 8, measurement: "tbsp"}
6: {name: "non-dairy milk of your choice (I use Almond)", amount: 6, measurement: "cup/s"}
7: {name: "raisins (optional)", amount: 4, measurement: "handful/s"}
8: {name: "rolled oats", amount: 4, measurement: "cup"}
length: 9
__proto__: Array(0)

and it will just keep doubling every time I add something new. And the actual page with the ingredients (that should say 1 of most things) looks like this:

screenshot

Thanks again for all your input.


Solution

  • You are changing the structure of an array in sort function, and that is big no-no. Sort function returns a new array that is sorted according to sort function, it does not alter the original array. You should first use reduce to merge duplicates, and then sort an array.

    userSL.reduce((reduced, element) => {
      const index = reduced.findIndex(r => r.name.toLowerCase() === element.name.toLowerCase();
    
      if (index === -1) return [...reduced, element];
    
      reduced[index].amount += element.amount;
    
      return reduced;
    }, []).sort((a, b) => {
      const nameA = a.name.toLowerCase()
      const nameB = b.name.toLowerCase()
    
      if (nameA > nameB) return 1;
      if (nameA < nameB) return -1;
    
      return 0
    })
    

    In reduce we are adding one by one element to a resulting array (reduced), but if we find out that current element has already been added, we simply update amount of already added element. Sort is then straight forward, since array is already merged.

    This is not the most efficient implementation, and it may not compile (I'm writing it directly here), but idea is simple and should work.