Search code examples
javascriptember.jsember-datarelationshipember-controllers

Ember 2, filter relationship models (hasMany, belongsTo) and calculate computed property based on relationships


These are my files:

Models

app/models/basket.js:

export default DS.Model.extend({
  name: DS.attr('string'),
  house: DS.belongsTo('house', { async: true }),
  boxes: DS.hasMany('box', { async: true })
});

app/models/box.js:

export default DS.Model.extend({
  qty: DS.attr('number'),
  basket: DS.belongsTo('basket'),
  cartLines: DS.hasMany('cart-line', { async: true })
});

app/models/cart-line.js:

export default DS.Model.extend({
  qty: DS.attr('number'),
  box: DS.belongsTo('box'),
  product: DS.belongsTo('product')
});

app/models/product.js:

export default DS.Model.extend({
  name: DS.attr('string'),
  price: DS.attr('number')
});

Routes

app/routes/basket.js:

export default Ember.Route.extend({
  model(params) {
    return Ember.RSVP.hash({
      basket: this.store.findRecord('basket', params.basket_id),
      boxes: this.store.findAll('box'),
      products: this.store.findAll('product')
    });
  },
  setupController(controller, models) {
    controller.setProperties(models);
    }
});

Controllers

app/controllers/basket.js:

export default Ember.Controller.extend({
  subTotal: Ember.computed('[email protected]', function () {
    return this.products.reduce((price, product) => {
      var total = price + product.get('price');
      return total;
    }, 0);
  })
});

Questions:

I'm newbie, so I'm studying and makings mistakes. Sorry.

1) Which is the best Ember way to filter relationships when I first enter in route? For example now I load every box in my app whith boxes: this.store.findAll('box'). I need a way to not load all the box in my webapp, just the one in basket. I need the "query with filter" directly from a backend?

UPDATED QUESTION 2) Which is the best Ember way for calculate subTotal? Now, with code below, Ember gives me the subTotal but just in console.log(tot) and after the promises! Why this? How can I wait the promises? I don't understand what to do:

subTotal: Ember.computed('[email protected]', function () {
  let count = 0;
  console.log('subTotal called: ', count);
  // It should be 0 ever
  count = count + 1;

  return this.get('basket.boxes').then(boxes => {
    boxes.forEach(box => {
      box.get('cartLines').then(cartLines => {
        cartLines.reduce(function (tot, value) {
          console.log('tot:', tot + value.get('product.price'));
          return tot + value.get('product.price');
        }, 0);
      });
    });
  });
});

It gives me in template [object Object] because I'm also using in hbs {{log subTotal}} and in console it gives me this:

subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember802", __ember_meta__: Meta}
subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember934", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember934", __ember_meta__: Meta}
subTotal called:  0
ember.debug.js:10095 Class {__ember1476746185015: "ember1011", __ember_meta__: Meta}
ember.debug.js:10095 Class {isFulfilled: true, __ember1476746185015: "ember1011", __ember_meta__: Meta}
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27
tot: 3.5
tot: 6
tot: 13.5
tot: 21
tot: 24.5
tot: 27

Why it shows three times subTotal called: 0, no matter if there are zero or one or a thousand products. He always calls three times subTotal called: 0, why?

Is it good to use computed properties with promises?

3) Am I right with that relationship encapsulation?

UPDATED QUESTION 2:

Now I'm using this code, but without success:

import Ember from 'ember';
import DS from 'ember-data';

export default Ember.Controller.extend({

  totalCount: Ember.computed('[email protected]', function () {
    let total = 0;
    const promise = this.get('basket.boxes').then(boxes => {
      boxes.map(box => {
      // const trypromise = boxes.map(box => {
        console.log('box:', box);
        box.get('cartLines').then(cartLines => {
          console.log('cartLines:', cartLines);
          const cartLinesPromise = cartLines.map(cartLine => {
              console.log('cartLine:', cartLine);
              // return cartLine.get('qty');
              // return cartLine;
              // });
              return {
                qty: cartLine.get('qty'),
                price: cartLine.get('product.price')
              };
              //     return cartLines.map(cartLine => {
              //       console.log('cartLine:', cartLine);
              //       return cartLine.get('qty');
              //       //   return {
              //       //     qty: cartLine.get('qty'),
              //       //     price: cartLine.get('product.price')
              //       //   };
              //     });
            })
            // });
        return Ember.RSVP
          .all(cartLinesPromise)
          .then(cartLinesPromise => {
            console.log('cartLinesPromise:', cartLinesPromise);
            // cartLinesPromise.reduce((tot, price) => {
            //   console.log('tot:', tot);
            //   console.log('price:', price);
            //   console.log('tot+price:', tot + price);
            //   return tot + price, 0;
            // });

            return total = 10;
            // return total;
          })
        });

      });

      // return total;
    });

    return DS.PromiseObject.create({ promise });
  })

})

Comments are for many try.

In template I use:

{{log 'HBS totalCount:' totalCount}}
{{log 'HBS totalCount.content:' totalCount.content}}
Total: {{totalCount.content}}

But promise have null content.

Where I'm wrong?

Any incorrect return?

Is this code "promising" correct?


Solution

  • There is nothing bad to being new to technology, especially when your question is well formatted and think through.

    1) Which is the best Ember-Data way to filter relationships?

    This is complex question with a lot of possible endings.

    The easiest thing to do is just ask on that model.

    Ask on basket

    Given your model you can do:

    model(params) {
      // we will return basket but make boxes ready
      return this.get('store').find('basket', params.basket_id).then(basket => {
        return basket.get('boxes').then(() => basket);
      });
    }
    

    But this has few limitations and advantages

    • you need to send ids with basket
    • you have to enable coalesceFindRequests to make it sane
    • it will load only boxes that are not in store

    Edit: you need to send ids with basket This means that basket in your payload will have to provide identification for it's boxes. In case of rest api: {basket: {id: 1, boxes: [1,2,3], ...}. It will then check which ids are not loaded into the store already and ask api here (assuming that box with id 2 is already loaded): /boxes?ids[]=1&ids[]=3.

    Ask yourself

    model(params) {
      const store = this.get('store');
      const basket = params.basket_id;
    
      return RSVP.hash({
        model: store.find('basket', basket),
        boxes: store.query('box', {basket}),
      });
    },
    
    • On the other hand this approach will send request for basket only if basket is not in store already (same as before) but always query for boxes(if you don't like it you would have to use peekAll and filter to check if you have all of them or smt like that).
    • Good think is that the requests will be parallel not serial so it may speed things up.
    • Basket also doesn't have to send ids of its boxes.
    • You can do server side filtering by altering query param

    Edit: if you don't like it you would have to use peekAll and filter to check if you have all of them You can actually check that with hasMany.

    Sideload them

    Instead of sending two requests to server you can make your api so that it will append boxes into the payload.

    Load only basket and let rest to load from template

    You can load only bare minimum (like load only basket), let ember continue and render the page. It will see that you are accessing basket.boxes property and fetch them. This wont look good on its own and will need some additional work like spinners and so on. But this is one way how to speed up boot and initial render time.

    2) Which is the best Ember way for calculate subTotal

    You want to calculate sum of something that is three levels deep into async relationships, that's not going to be easy. First of I would suggest putting totalPrice computed property into basket model itself. Computed properties are lazily evaluated so there is no performance degradation and this is something that model should be able to provide.

    Here is little snippet:

    // basket.js
    const {RSVP, computed} = Ember;
    
    price: computed('[email protected]', function() {
      const promise = this.get('boxes').then(boxes => {
        // Assuming box.get('price') is computed property like this
        // and returns promise because box must wait for cart lines to resolve.
        const prices = boxes.map(box => box.get('price'));
    
        return RSVP
          .all(prices)
          .then(prices => prices.reduce((carry, price) => carry + price, 0));
      });
    
      return PromiseObject.create({promise});
    }),
    

    You would need to write something like this for each level or give up some of the async relations. The problem with your computed property is that [email protected] wont listen on everything that can change overall price (for example change of price of product itself). So it won't reflect and update on all possible changes.

    I would sagest to give up some async relations. For example request on /baskets/2 could sideload all of its boxes, cartLines and maybe even products. If your api doesn't support sideloading, you can fake it by loading everything in route (you would have to use second example - you are not allowed to access boxes before they are in the store in case of async: false). That would lead to much simpler computed properties to calculate total price and in case of sideloading also reduce stress on server and clients confections.

    // basket.js
    const {computed} = Ember;
    
    boxes: DS.hasMany('box', {async: false}),
    
    price: computed('[email protected]', function() {
      return this.get('boxes').reduce(box => box.get('price'));
    }),
    

    Update and overall after thoughts

    I don't think that doing all sums in one function is viable, doable or sane. You will end up in callback hell or some other kind of hell. Moreover this is not going to be performance bottleneck.

    I made jsfiddle it is basicaly more fleshed out version of snippet above. Note that it will properly wait and propagate price which is two promises deep and also should update when something changes (also I didn't test that).