Modeling a shopping cart in ember-data


#1

As a test project, I’m building a commerce application with ember and ember-data. Things have been going well and I’m having fun learning, but I’m stumped on how to correctly model (and use) a cart using ember-data.

There are a few good resources and how-to’s out there that have helped, like the ember-cart source, the yoember product app, and a emberscreencast series. These all basically use the same pattern: cart items are stored in an array on the cart service (which is injected).

I initially went that route, but have come to the realization that the cart data needs to be persisted (or at least regularly validated) against the back-end, for these reasons. Since ember-data acts as both a store and a back-end persistence method, it seems pretty well suited. My idea was, instead of keeping the cart items as a property on the service, create a cart model and store the data there. I went down that path and kind of have that working…

The Ember-Data Setup

To start, my cart model is extremely simple. Just a model with a single property that is an array:

export default DS.Model.extend({
	cartitems: attr('')
});

With a cart, obviously you need to have computed properties that “watch” the cart and provide running totals of quantities and item prices. With some help from typerturdenpants (love it) on the slack channel, I got a computed property to work on my ember-data cart model using peekRecord so the API doesn’t get pummeled. Then from that property I can build the others. That looks like:

	cartObj: null, // set on init with: set(this, 'cartObj', this.get('store').peekRecord('cart', id));

	lineItems: computed('cartObj.cartitems.[]', function() {
		let cartItems = get(this, 'cartObj.cartitems');
		console.log("LINEITEMS COMPUTED", cartItems);
		return cartItems;
    }),
	itemPrices: computed.mapBy('lineItems', 'itemPrice'), // array of all cart item prices
    total: computed.sum('itemPrices'), // cart total
	itemQuantities: computed.mapBy('lineItems', 'quantity'), // array of all cart item quantities
	totalQuantities: computed.sum('itemQuantities'), // total quantity of all cart items

The Problem

I can add items…but it’s wonky. I put a console.log in the computed property and I can see it getting hit 3X every time an item is added (image). There are also 3 network requests (image). I can kind of understand why: checking store, adding locally, getting OPTIONS, GETing cart from API, POSTing cart back to API. But is this the right way to go? It seems really clunky. You can actually see this process happening in the UI (video clip).

I’m also not seeing the data live-update on my /basket route that I have open in a different tab, even though the cart service is injected into it. Which makes me think this is just all wrong.

Here’s what my ‘add’ method looks like in the cart service:

add(item) {

	this.get('store').findRecord('cart', get(this, 'cartId')).then(cart => {
	
		let existingLineItem = get(this, 'lineItems').findBy('skuid', item.skuid);

		if (existingLineItem) { // if item is already in cart, just add more
			set(existingLineItem, 'quantity', parseInt(existingLineItem.quantity)+parseInt(item.quantity))
		} else {
			cart.get('cartitems').addObject(item); // item is not in cart, add it
		}
		cart.save();
	});
	return true;
}

So, I dunno if I am even approaching this right. I am wondering if I should ditch ember-data for the cart and just use service properties like all the cart examples I found do, and just use ember-network at certain points to validate the cart data against the back end.

But it really just seems like ember-data should work for this. What am I missing here?


#2

Hey @midget2000x it does sound like you’re close and I don’t see any reason why you couldn’t use Ember Data for your cart.

I have some questions and a few notes. I’m just kinda spitballing here so forgive me if any of these are dumb:

It sounds like you have a service for your cart as well, is that the case? If it were me I’d probably use Ember Data within a service. A service is ideal for for keeping application-level state, like a cart. I don’t see anything wrong with using Ember Data for the cart, I’d encourage it actually. I think you’ll just need to massage it a bit more to get it all working.

Have you played around with the observer path on the computed property? Like using ‘cartObj.cartitems.length’ or just ‘cartObj.cartitems’ instead of ‘cartObj.cartitems.[]’? Sometimes that can change the way the computed property gets called. The path you currently have will recalculate the CP every time any of the cart items changes, whereas I’d guess you’d only want it to recalculate when the number of items changes?

What sort of backend are you using? If you had the app open in another tab (which means another application instance, no data shared between the two tabs) I would expect it to update the /basket route only if you were polling for cart records or the server was pushing cart updates via websockets or something. Otherwise the second tab would be sitting on the basket route and have an out of date cart record because its cart record is never getting updated.

It’s up to you how often you want to update the cart from the server side but it may not be often enough to do it only when an item is added. I would probably consider doing the findRecord in a different method and letting ‘add’ only take care of the business logic of adding a field to that record. You could do a periodic poll on the cart record, for example, which would keep it updated across app instances. Then in your add method all you do is get the local cart record reference, add the new item to it and save. No async logic needed.

Are the ‘items’ being added to the cart also Ember Data models? If so you could consider using a relationship on the cart model instead of a default non-transformed attribute (what I would call a “raw” attribute: DS.attr(’’)). A relationship might get you better behavior here and there are a lot of nice things about them that I’d think would fit well with the cart concept. If the items aren’t also Ember Data models then a raw transform like you have should probably work fine. A note on that: we use raw attributes in our app but we specify a type (DS.attr(‘raw’)) and specify a transform for the ‘raw’ type to make sure it is correctly munged. Not sure you’ll need to do that but it may be a little safer.


#3

I sincerely appreciate the help, @dknutsen. I’m a newb and still very much in the messing around/figuring out mode. Basic ember stuff is easy to find on the web, but one you get into specific use cases, guidance is harder to come by. So thanks!

That’s correct, and yes, I’m using ember-data within the service. The cart service is initialized in an instance-initializer and injected into controllers and components from there. If you think that’s not the best approach, let me know. I’ll include the complete code that I currently have in that service at the end of this post.

I haven’t, that’s a good thought. The obstacle I see there is that my “mini-cart” shows (line items * quantities) for the quantity-in-cart sum, not just a count of line items. So I think I’ll need to actually access the quantities for each line item from the model.

Just a regular 'ol http API. Thanks for the explanation - I actually didn’t realize separate tabs were different instances (like I said…newb!)

I agree with you there. For now I was just trying to see if ember-data was going to work for the cart, but yeah I should think a little broader. The API is going to be the “source of truth” for item data so it will need to be polled regularly.

As it sounds like you are actually using ember-data for a cart currently, I want to understand what you mean about this:

Do you mean the ‘cart’ model itself should be a computed property? Can you type out some code or pseudo-code? It sounds like something that could get me over the hump on this.

Actually…I started down the road of having separate but related ‘cart’ and ‘cartitems’ models initially. I got into a similar bind and posted about it on stackoverflow. I got no takers so I decided to try the simpler approach of just the “raw” cartiitems DS.attr(’’)) in a single cart model to see if ember-data was going to work.

I think the multiple related models makes more sense from an architectural standpoint. In fact, the ‘cartitems’ model needs to be related to yet another model - ‘products’ - so that general product data like name and image don’t need to be stored in 2 places. But I ran into a wall with how to express all this in the code. Hours would pass with me tweaking various get()'s, set()'s, then()'s and save()'s and I’d end up nowhere! :smiley:

So yeah, thanks for the help and any examples or further guidance you want to throw my way is appreciated!

Here’s my rough-but-klnd-of-works cart.js service:

import Ember from 'ember';
const { Service, inject, computed, set, get } = Ember;
export default Service.extend({

	store: inject.service(),
	cartId: null,
	cartObj: null, // set on init...instance-initializer is used
	lineItems: computed('cartObj.cartitems.[]', function() {
		let cartItems = get(this, 'cartObj.cartitems');
		return cartItems;
    }),
	itemPrices: computed.mapBy('lineItems', 'itemPrice'), // array of all cart item prices
    total: computed.sum('itemPrices'), // cart total
	itemQuantities: computed.mapBy('lineItems', 'quantity'), // array of all cart item quantities
	totalQuantities: computed.sum('itemQuantities'), // total quantity of all cart items

	// cart methods
	add(item) {
		this.get('store').findRecord('cart', get(this, 'cartId')).then(cart => {
			let existingLineItem = get(this, 'lineItems').findBy('skuid', item.skuid);

			if (existingLineItem) { // if item is already in cart, just add more
				set(existingLineItem, 'quantity', parseInt(existingLineItem.quantity)+parseInt(item.quantity))
			} else {
				cart.get('cartitems').addObject(item);
			}
			cart.save();
		});
		return true;
    },
	loadCart(id) {
		// called from instance-initializer when a previous cart id is found in localStorage
		let cartObj = this.get('store').findRecord('cart', id).then(response => {
			set(this, 'cartObj', this.get('store').peekRecord('cart', id));
			set(this, 'cartId', id);
		});
    },
	createCart() {
		// called from instance-initializer when no prev cart id is found
		let cart = this.get('store').createRecord('cart');
		cart.save().then(response => {
			set(this, 'cartId', response.get('id'));
			set(this, 'cartObj', this.get('store').peekRecord('cart', response.get('id')));
			window.localStorage.setItem('cart', response.get('id')); // instead of cookie
		});
    },
	clear() { /* not implemented yet */ },
	remove() { /* not implemented yet */ }
});

#4

I wouldn’t call you a newb, your code is solid and follows a lot of Ember conventions pretty well. I think there’s just a couple tricky things at work here which may be tripping you up. Well and the architectural concerns at this level can be tricky. Also I should mention that while I’ve never written a cart service, I have written several that use the store.

So I think I’ll need to actually access the quantities for each line item from the model.

In that case maybe try:

lineItems: computed('cartObj.cartitems.@each.quantity', 'cartObj.cartitems.length', function() {

Because then you’re basically saying “update this CP every time the quantity of any cart item changes, and also when the number of cart items changes”. It’s a little more specific, and may prevent the CP from firing too often.

Do you mean the ‘cart’ model itself should be a computed property? Can you type out some code or pseudo-code? It sounds like something that could get me over the hump on this.

Not quite what I meant, but maybe it’s best to just show you. See the code below.

As far as the models go… I guess it is a little overcomplicated to have to worry about the intermediate cartitems model so you’re probably on the right track here. Guess you can always refactor and add more complexity later huh?

I took a crack at modifying your code (below), I’m not sure it will fix everything, and you’ll want to test it carefully, but it may get you somewhere. Honestly it sounds like the biggest problem you’re having is with your computed property, not necessarily Ember Data. Playing with the observers might be what you need. I changed them below, but you’ll probably want to do some testing with a few different things.

One thing I did change was some of the ceremony around creating/updating records. Since record promises resolve to records you can be a little more concise when dealing with them (vs having to do lots of peekRecords).

I also threw in a basic poller at the bottom. I didn’t test any of it but it’s pretty straightforward and we do a lot of things like this in our code.

Anyway, play around with it. I’ll keep checking back here so if you have any other questions or problems I may be able to help. I think you’re very close though!

import Ember from 'ember';
const { Service, inject, computed, set, get } = Ember;
export default Service.extend({

    store: inject.service(),
    cartObj: null, // set on init...instance-initializer is used
    lineItems: computed('cartObj.cartitems.@each.quantity', 'cartObj.cartitems.length', function() {
        let cartItems = get(this, 'cartObj.cartitems');
        return cartItems;
    }),
    itemPrices: computed.mapBy('lineItems', 'itemPrice'), // array of all cart item prices
    total: computed.sum('itemPrices'), // cart total
    itemQuantities: computed.mapBy('lineItems', 'quantity'), // array of all cart item quantities
    totalQuantities: computed.sum('itemQuantities'), // total quantity of all cart items

    // cart methods
    add(item) {
        let cartObj = get(this, 'cartObj');

        let existingLineItem = get(this, 'lineItems').findBy('skuid', item.skuid);

        if (existingLineItem) { // if item is already in cart, just add more
            set(existingLineItem, 'quantity', parseInt(existingLineItem.quantity)+parseInt(item.quantity))
        } else {
            get(this, 'cartObj.cartitems').addObject(item);
        }
        cart.save();
    },
    loadCart(id) {
        let self = this;
        // called from instance-initializer when a previous cart id is found in localStorage
        let cartObj = self.get('store').findRecord('cart', id).then(response => {
            set(self, 'cartObj', response);
        });
        // you may even be able to do the below instead of the above, would need to test
        //set(this, 'cartObj', this.get('store').findRecord('cart', id));
 
        // start poller
        this._pollCart();
    },
    createCart() {
        let self = this;
        // called from instance-initializer when no prev cart id is found
        let cart = this.get('store').createRecord('cart');
        cart.save().then(response => {
            window.localStorage.setItem('cart', response.get('id')); // instead of cookie

            // start poller
            self._pollCart();
        });
        set(this, 'cartObj', cart); // you can do this whenever, no need to wait until the 'save' returns
    },
    clear() { /* not implemented yet */ },
    remove() { /* not implemented yet */ },


    // this "poller" basically just triggers a background refresh from the API (via findRecord)
    // every 5 (configurable) seconds
    _pollDuration: 5000, // 5 seconds
    _pollCart: function(){
        this.get('store').findRecord('cart', get(this, 'cartObj.id'));
        // set this pollerTimer on self so if for whatever reason we want to cancel it we can
        this.set("pollerTimer", Ember.run.later(this, this._pollCart, this.get("_pollDuration")));
    },
    // may never need this but here it is all the same
    _cancelPoll: function(){
      Ember.run.cancel(this.get("pollerTimer"));
    }
});


#5

@dknutsen, wow - thanks SO MUCH! Those changes work very well. both the API and CP are getting hit fewer times and no more flicker! This was very helpful, and I learned alot.

I like the idea of an cancellable automatic cart poller. I can be sure everything’s up to date, and maybe cancel it after a certain amount of inactivity. Only change I needed to make was to move this._pollCart(); to inside the promise resolution in the loadCart method and it’s working!

Well, since I have ya here…if you were approaching this project would you implement the intermediate cartitems model? That was my initial approach because I thought having well-defined, related models would help me harness all the built-in goodies ember-data provides (as I mentioned, it would be good to relate cartiitems to products - though it’s not super-essential).

But I certainly don’t want to bite off more than I can chew!

Again thanks so much, I really appreciate it!


#6

Awesome! Glad you got it working well. Good call on the poller start too, that wasn’t great code I wrote :grin:

Well, since I have ya here…if you were approaching this project would you implement the intermediate cartitems model?

I probably wouldn’t. I think in the past my tendency has been to over-abstract or abstract too early. In fact sometimes I still do that. It’s an easy path to go down, but I’ve learned to try and avoid it. When thinking about problems like that my general rule of thumb is that if there isn’t a clear and obvious benefit immediately, or in the forseeable (planned) future, abstracting something isn’t worth it. If you run up against an issue in the future that would greatly benefit from that extra abstraction you can always refactor later. In this case, you have a nice simple design that works, and unless you knew you were going to add cart features that would specifically benefit from the intermediate model, it seems like it’s worth saving yourself the added complexity. Often simplicity is the best solution. It’s rarely black and white, and so it’s usually a tough call, but I would say it’s best to avoid unnecessary abstractions.

While I’m rambling on about vague principles, here are a few more that I’ve learned (mostly the hard way) about Ember (and general) development that may be helpful. Unfortunately none of them are hard and fast rules they’re more general guidelines, but good to keep in mind:

Don’t avoid using controllers - because that is (probably) the eventual direction that Ember will go many people advocate completely avoiding controllers. However more reasonable members of the community seem to agree that until there is a clear way forward (routable components probably), controllers are a normal and often necessary part of an Ember app. So I’d ignore anything that says otherwise. Use Controllers in the way they were intended and you may save yourself a lot of trouble.

Don’t over-componentize - similar to “don’t overabstract” and “don’t avoid controllers”, it doesn’t do you any good if you’re trying to squeeze controller code into a component unnecessarily or if you end up with a really complex component or network of components because you abstracted them too soon. Components can be tricky to wire up and if you abstract them out too soon you could add unnecessary complexity

Don’t be dogmatic with DDAU - DDAU (data down actions up) is a great paradigm for many components, but that doesn’t mean you should go out of your way to make EVERY component work with DDAU. Sometimes a component can/should modify data and that’s ok. If it’s in the scope of concerns or it would be a huge and unnecessary refactor just to switch it to DDAU it may not be worth it. Blind application of DDAU (or any other programming paradigm for that matter) isn’t good.

Try to let the router handle as much of your async data fetching as possible - async data fetching is a really challenging problem, and the router solves it really well (if you’ve ever tried to fetch data in a component or controller you may have run into frustrating issues). Of course there are many instances where you may need to make async requests in a controller or ocmponent (another thing not to be dogmatic about with components, sometimes it just makes sense for them to fetch their own data). In those cases I’d recommend using something like Ember Concurrency. It’s a great addon that re-solves a lot of those async problems for you. There was a great talk on it at EmberConf this year.

Avoid using observers when possible - (not to be confused with Computed Properties of course) sometimes you need them but they can cause a lot of issues so if you can use CP’s or something like Ember Concurrency, or even just a regular old function instead of an observer you may be doing yourself a favor.

Be careful with computed property observer paths - as you saw in this example, a seemingly innocuous CP observer path can behave differently than expected. I’ve done this more times than I can count and it can cause some big issues. Another annoying thing is that it’s really difficult to debug CP’s (in the sense that you don’t know what called it). So when writing your CP’s be very specific and very careful with your observer path strings.

A few more resources

And… guess that’s all I have off the top of my head. Hope some of that is helpful to you or someone else. I’d say based on your code you’re killing it and you’ll be an expert in no time. Keep it up!


#7

Thanks so much man! This is all great info. I’m trying to drink up as much ember guidance as possible, so I will spend some time with this material. Thanks again!!