问题
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('boxes.@each.cartLines', 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('basket.boxes.@each.cartLines', 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('basket.boxes.@each.cartLines', 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?
回答1:
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('boxes.@each.price', 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 boxes.@each.cartLines
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('boxes.@each.price', 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).
回答2:
The solution to your question is nicely explained in How to return a promise composed of nested models in EmberJS with EmberData? by @Kingpin2k.
What you want to do is just load a basket and its associated models(box, cat-line and product) rather than loading all boxes, cartLines and products. Also to compute the subTotal, we would need all those dependency promises resolved beforehand. Following the solution given in the post mentioned earlier, your solution would look like:
MODEL: app/models/cart-line.js
export default DS.Model.extend({
qty: DS.attr('number'),
box: DS.belongsTo('box'),
product: DS.belongsTo('product', { async: true })//assuming you are not side-loading these
});
ROUTE: app/routes/basket.js
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('basket', params.basket_id).then((basket)=> {
return basket.get('boxes').then((boxes)=> {
let cartLinesPromises = boxes.map(function (box) {
return box.get('cartLines');
});
return Ember.RSVP.allSettled(cartLinesPromises).then((array)=> {
let productPromises = array.map(function (item) {
return (item.value).get('product');
});
return Ember.RSVP.allSettled(productPromises);
});
});
});
}
});
CONTROLLER: app/controllers/basket.js
subTotal: computed('model.boxes.@each.cartLines', function () {
//you dont need to use DS.PromiseArray because the promises all have been already resolved in the route's model hook
let total = 0;
this.get('model.boxes').forEach((box)=> {
box.get('cartLines').forEach((cartLine)=> {
total += cartLine.get('product.price');
});
});
return total;
})
Lastly, for the issue you were having here:
subTotal: computed('boxes.@each.cartLines', function() {
return DS.PromiseArray.create({
//"this" here is DS.PromiseArray object and not your controller instance
promise: this.get('boxes').then(boxes => {
return boxes.filter(i => i.get('cart-line'));
})
});
})
you would not use the computed construct if following the solution given above, but just wanted to point out solution in similar conditions.
subTotal: computed('boxes.@each.cartLines', function() {
let controllerInstance = this;
return DS.PromiseArray.create({
promise: controllerInstance.get('boxes').then(boxes => {
return boxes.filter(i => i.get('cart-line'));
})
});
})
来源:https://stackoverflow.com/questions/40032747/ember-2-filter-relationship-models-hasmany-belongsto-and-calculate-computed