Sum list items / present running total


#1

I need some help determining where to place the summing function and display a running total. I think I have a good start, but need guidance. I am happy to send coffee money via paypal for the help : )

I am working with the Ionic “Super” template and modifying the list-master.ts and list-master.html pages (code is below the screenshot).

Here is a picture of the screen view I’m working with (I need the total to be the sum of the list items):

I know the function has not been called in the examples. I’m not sure where to call it.

list-master.ts (look for // Sum list items near bottom)

import { Component } from '@angular/core';
import { IonicPage, ModalController, NavController } from 'ionic-angular';

import { Item } from '../../models/item';
import { Items } from '../../providers/providers';

@IonicPage()
@Component({
  selector: 'page-list-master',
  templateUrl: 'list-master.html'
})
export class ListMasterPage {
  currentItems: Item[];

  constructor(public navCtrl: NavController, public items: Items, public modalCtrl: ModalController) {
    this.currentItems = this.items.query();
  }

  /**
   * The view loaded, let's query our items for the list
   */
  ionViewDidLoad() {
  }

  /**
   * Prompt the user to add a new item. This shows our ItemCreatePage in a
   * modal and then adds the new item to our data source if the user created one.
   */
  addItem() {
    let addModal = this.modalCtrl.create('ItemCreatePage');
    addModal.onDidDismiss(item => {
      if (item) {
        this.items.add(item);
      }
    })
    addModal.present();
  }

  /**
   * Delete an item from the list of items.
   */
  deleteItem(item) {
    this.items.delete(item);
  }

  /**
   * Navigate to the detail page for this item.
   */
  openItem(item: Item) {
    this.navCtrl.push('ItemDetailPage', {
      item: item
    });
  }

  // Sum list items
  public sum : number = 0;
  public total(items){
    this.sum = 0;
    for(let i = 0; i<this.items.length; i++){
      this.sum = this.sum + this.items[i];    
    }
    return this.sum;
  }
}

list-master.html ( look for H1 tag Total: near bottom)

<ion-header>

  <ion-navbar>
    <ion-title>{{ 'LIST_MASTER_TITLE' | translate }}</ion-title>
  </ion-navbar>

</ion-header>

<ion-content>
  <div padding>
    <button ion-button (click)="addItem()" block>{{'Add Item ' | translate }}
    </button>
  </div>  
  <ion-list>
    <ion-item-sliding *ngFor="let item of currentItems">
      <button ion-item (click)="openItem(item)">
        <h1>{{item.price | currency: 0 }}</h1>
        <p>{{item.description}}</p>
      </button>
      <ion-item-options>
        <button ion-button color="danger" (click)="deleteItem(item)">
          {{ 'DELETE_BUTTON' | translate }}
        </button>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
  <ion-card>
  <ion-card-content>
    <h1>Total: {{ this.sum | currency: 0 }}</h1>
  </ion-card-content>
</ion-card>
</ion-content>

I would prefer to total the items using .reduce like this:
let items = [ ];
let adder = (accum, currentVal) => accum + currentVal;
let total = items.reduce(adder);

Thanks in advance for reading this and any help!!


#2

What I would do is call total() only once, upon construction. Thereafter, I would modify sum directly on every addition and deletion. That would make those operations constant time. The other option would be to call total() on every addition/deletion, which would be wasteful but probably not noticeably so unless your list gets large.


#3

You could always have your template say

Total: {{ arraySum(currentItems) | currency: 0 }}

where arraySum(arrayToSum: Array<number>): number is your sum-with-reduce method.

That said, the previous comment about time inefficiency is why I wouldn’t do it this way, personally (though your users might not notice the difference). I’d keep a running total in a variable, and add the price of a new item to it, instead of recomputing the subtotal every time.

Re: your offer to pay for a solution: today’s the 50th Anniversary of the assassination of MLK Jr. If you get a solution here “for free,” perhaps you could think about making a small donation to an organization that fits the occasion.


#4

Thanks for this idea. I will work on this.


#5

I go to pretty great lengths to avoid function calls in template expressions, because they get called on every change detection cycle, and there are zillions of those. If you do use them, it’s probably worth your time to familiarize yourself with the OnPush change detection strategy.


#6

How is it that member functions of the class ListMasterPage have access to the “item” and “items” variables, yet when I pass “items” or “currentItems” to my total() I get errors telling me that it cannot read property of “items” or “currentItems”.

This is an original member function that works as expected:

addItem() {
    let addModal = this.modalCtrl.create('ItemCreatePage');
    addModal.onDidDismiss(item => {
      if (item) {
        this.items.add(item);
      }
    })
    addModal.present();
  }

My added member function getting errors that “items” is undefined.

public sum : number = 0;
  public total(items){
    this.sum = 0;
    for(let i = 0; i<this.items.length; i++){
      this.sum = this.sum + this.items[i];    
    }
    return this.sum;
  }
}

I am ok with calling the total() on every addition/deletion as there will only be a few items for every transaction.

I may be working in the wrong file as there is also an Items class in src/mocks/providers/items.ts

import { Injectable } from '@angular/core';

import { Item } from '../../models/item';

@Injectable()
export class Items {
  items: Item[] = [];

  defaultItem: any = {
    "price": 0.00,
    "description": "Latte",
  };


  constructor() {
    let items = [];

    for (let item of items) {
      this.items.push(new Item(item));
    }
  }

  query(params?: any) {
    if (!params) {
      return this.items;
    }

    return this.items.filter((item) => {
      for (let key in params) {
        let field = item[key];
        if (typeof field == 'string' && field.toLowerCase().indexOf(params[key].toLowerCase()) >= 0) {
          return item;
        } else if (field == params[key]) {
          return item;
        }
      }
      return null;
    });
  }

  add(item: Item) {
    this.items.push(item);
  }

  delete(item: Item) {
    this.items.splice(this.items.indexOf(item), 1);
  }
}

I truly appreciate any advice.


#7

I don’t really think that way in Javascript, so I’m probably not the best person to parse your code. But if you want more of a philosophical/ranty answer, here goes.

I don’t use classes to define objects in Javascript, and I don’t put member functions inside objects. Instead, I define objects with interfaces, and create a provider that manages that object type. The data of the widgets lives in the provider, and pages can request the information, or request to modify the information. (WidgetManager provider for objects defined in the Widget interface.) So it would look something like

this.widgetManager.addWidgetPriceToCurrentTotal(widgetInFocus)

I know I’m not alone in this approach to objects and functions. I think it is the safest way to go with Javascript, because not even “constants” are immutable, so it’s easy to overwrite information unless there’s a central data management point. So that’s my recommendation, for whatever it;s worth.


#8

Follow up question regarding summing list items


#9

I agree 100% with everything @AaronSterling said about design. I refer to it as “dumb data”, where one uses only interfaces for holding information, and all the logic needed to manage it is in providers.

As for why that total function is not doing what you intend it to, the items parameter that is being passed into it is never used anywhere. It’s completely separate from this.items. That function also breaks a relatively important rule for me, which is that functions should either modify state or return the result of their operation, but not both. Finally, given what you’ve written so far, I’m surprised you don’t end up with sum being "[object Object][object Object][object Object]" ad infinitum for the number of members of this.items, because instead of summing item.price, you’re applying the + operator to an Item object. I believe what JavaScript would do in that situation would be to coerce the object into a string and then concatenate it. No arithmetic involved at all, and very doubtful what you want.

Since it is only two lines, why are you so averse to simply adding this.sum += item.price to add() and this.sum -= item.price to remove()?


#10

I was getting the [object Object] as sum so thanks for that explanation.

Your solution is great for getting the data to display, however the data being processed is clearly a string. I’m getting NaN and undefined1234, so my array data is not defined.

I’m stumbling my way through Angular/Ionic and OOP in general. I appreciate your help greatly.

If you would like to take me up on the coffee offer you can PM your paypal email.

The following image is for documenting:


#11

This is one of several million things that I really hate about JavaScript. You have two options: either make certain that you are storing numbers in Item.price, or make the summation logic robust against the case where Item.price contains a string.

There are a few options, but probably your most useful tool is the unary plus operator. When you deploy it depends on which of the two roads in the previous paragraph you want to take: either when it is coming in from an external source (probably JSON) or inside the total() function.

$ node
> let a = 1.2;
undefined
> let b = 3.4;
undefined
> a+b
4.6

So far, we’re cruising, but here comes the monkey wrench:

> let c = "5.6";
undefined
> a + b + c;
'4.65.6'

The first ‘+’ is arithmetic, but then the second one becomes string conversion + concatenation, because its arguments are not both numbers. The unary plus says “hey stupid JavaScript, I said this is a god damn number, so stop trying to get cute”:

> +a + +b + +c;
10.2

Victory is ours!


#12

Best quote of 2018, and one of javascript’s most boneheaded behaviors. Especially boneheaded because of the nature of the task and how often programmers do it: add numbers.