2

Alright. I'm going to give in here and ask for some help. I think I'm running into multiple issues and not sure of the best approach. I'm using handlebars to create an ul of li that are my json objects to backbone models. Now that the template works I want to on click get the model to be able to add to another collection.

My first problem. I thought backbone defined a model id by default? If i set default to "" or null every model's id is "" or null. The json objects have an id I could assign to the backbone id but if I do that in the defaults id: jsonID, the jsonID is undefined. If I do object.toJSON() in the console there is no backbone created id. So I don't have a value to use in the handlebars template to assign my div id to the backbone id for that model. To then use that to get element id so that on click I could get the backbone id. Or at least I've read a lot of examples that do it that way.

My second issue, I think stems from requirejs. All the examples I see for click even use this.collection or this.model. On my View file those always return undefined, assuming this is because of requirejs. I tried this example in particular http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/. I'm wondering if I should just scrap using requirejs, it seems to cause more problems then help.

Here is my code so far, I deleted out my click function code because none of it was working.

Collection File:

define(['jquery', 'backbone', 'lodash', 'Models/GroceryItem'],

    function($, Backbone, _, GroceryItem) {

    var GroceryItems = Backbone.Collection.extend({

        model: GroceryItem,

        url: "data.json",

        parse: function(response) {
            return response.all_coupons;

        }

    });

    var storeItems = new GroceryItems();
    storeItems.fetch({
    success:function(){
        console.log(storeItems.toJSON());
        }
    });

    return storeItems;

});

View File:

define(['jquery', 'backbone', 'lodash', 'handlebars', 'Collections/GroceryItems'],

    function($, Backbone, _, Handlebars, storeItems) {



    var GroceryItemsView = Backbone.View.extend({

        template: Handlebars.compile(
            '<ul class="d-row">' +
                    '{{#each storeItems}}' +
                        '<li class="lineItem" id="{{coupon_id}}">' +    
                            '<div class="wrapper">' +
                                '<div class="header">{{coupon_title}}</div>' +              
                                    '<div class="column_wrapper">' +
                                        '<div class="two-col">' +
                                            '<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
                                            '<div class="description">{{coupon_description}}</div>' +
                                         '</div>' +
                                    '</div>' +
                                '<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +    
                            '</div>' +
                        '</li>' +
                '{{/each}}' +
            '</ul>'
            ),

        events: {
            "click li": "getModel"
        },

        getModel:function(e){

        },

        render: function() {
            var that = this;
            storeItems.fetch({
                success: function(storeItems) {
                    var storeTemplate = that.template({storeItems: storeItems.toJSON()});
                    that.$el.html(storeTemplate);
                    return that;
                }
            })          
            return this; 
        }
    });

    return GroceryItemsView;

});

Thanks a bunch for any help. It's much appreciated. If I'm going at this completely wrong, I'm open to any suggestions. I'm just learning backbone and javascript in general so I'm grinding away as I go with a lot of googling.

Thanks!

EDITED CODE:

define(['jquery', 'backbone', 'lodash', 'Collections/GroceryItems', 'Views/GroceryItemView'],

    function($, Backbone, _, storeItems, GroceryItemView) {

    var GroceryItemsView = Backbone.View.extend({

        tagName: 'ul',
        className: 'd-row',
        el: '#container',
        initialize: function () {
            //basically says only render when collection syncs
            this.listenTo(storeItems, 'sync', this.render);
        },

        render: function () {
            //got keep track of views for when you need close them (not important for now but you'll thank me later)
            this.groceryItemsView = [];
            storeItems.each(function (GroceryItem) {
                //we are making a new view for each model and passing it in as an option
                var itemView = new GroceryItemView({
                    model: GroceryItem
                });
                //The view creates an element but it is not attached to DOM. We will attach it to the ItemsView's $el (which IS attached to the DOM)
                this.$el.append(itemView.$el);

                this.groceryItemsView.push(itemView);
            }, this);
            }
        });

    var list = new GroceryItemsView();

    return list;

});

define(['jquery', 'backbone', 'lodash', 'handlebars', 'Views/GroceryItemsView', 'Models/GroceryItem'],

    function($, Backbone, _, Handlebars, GroceryItemsView, GroceryItem) {

        var GroceryItemView = Backbone.View.extend({

        template: Handlebars.compile(
            '<div class="wrapper">' +
                '<div class="header">{{coupon_title}}</div>' +
                    '<div class="column_wrapper">' +
                        '<div class="two-col">' +
                            '<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
                            '<div class="description">{{coupon_description}}</div>' +
                        '</div>' +
                    '</div>' +
                '<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +
            '</div>'
        ),

        tagName: 'li',

        className: 'lineItem',

        events: {
            'click': 'getModel'
        },

        initialize: function () {
           this.render();
        },

        getModel: function () {
            return this.model;
        },

        render: function () {
            this.$el.html(this.template(this.model.toJSON()));
        }
    });

    return GroceryItemView;
});
Trevor
  • 321
  • 2
  • 7
  • 21

2 Answers2

1

In backbone models there are actually two kinds of id's, the first is id which is meant to represent the id of your model on the server and isn't atomically assigned. The second is cid (client id) which backbone will atomically generate and assign for you.

In case your server model's id property isn't named id you can map it by setting the idAttributemodel property. For example if the id in your json model is called jsonID

var GroceryItems = Backbone.Collection.extend({

    model: GroceryItem,
    idAttributemodel: jsonID,

    url: "data.json",

    parse: function(response) {
        return response.all_coupons;

    }

});

Some more info on id, cid, idAttribute

I see that in your GroceryItems collection file you are both declaring your collection and instantiating it, it might make more sense in this case to just declare it and return and in your App View (or wherever you declare your collection view) instantiate it there and pass it to the view.

In order to retrieve the model id on the click event of the li, you have to options either you have a seperate view per each li which is bound to a specific model, or in your case where you are rendering all your models using the same view you can retreive it from the DOM.

For example

 getModel: function (e) {
        //you might want to consider using data attributes instead
        var modelId = $(e.currentTarget).attr('id'); 
        var model = this.storeItems.get(modelId);
    },

In general regarding using require.js I think that while there is a bit of a learning curve in the long run it is worth it. One thing you might want to consider doing is keeping one file per view/model/collection.

Community
  • 1
  • 1
Jack
  • 10,943
  • 13
  • 50
  • 65
  • Thank you for the help. I'm going to work through both of these solutions and try to get each of them to work. I'll report back once I've tested. – Trevor Jul 11 '14 at 20:15
0

The easiest way to get to a model of something you clicked is surprisingly simple.

  1. I STRONGLY recommend NOT relying on IDs. It's very bad practice. The whole point of using Models is to stop worrying about IDs :P
  2. Creating a Backbone View is not as expensive as some people say. It's actually quite efficient as long as you clean up properly. Break up every logical unit of DOM into it's own View. ESPECIALLY collections of Views
  3. Require is AWESOME. Don't give up on it. Once you figure it out you'll never want to go back. Just think of it as saving bunch of code from another file to a variable defined up top
  4. Don't use success option. Only listen to the sync event. Makes code cleaner, prevents loooooots of weird issues later on.

I haven't tested this code but the logic works (have done it many times)

//Preferrably keep this in a separate file or use require-handlebars
var itemTpl = Handlebars.compile(
    '<div class="wrapper">' +
    '<div class="header">{{coupon_title}}</div>' +
    '<div class="column_wrapper">' +
    '<div class="two-col">' +
    '<div class="product_image"><img src="{{coupon_thumb}}" alt="{{coupon_description}}" height="110" width="110"></div>' +
    '<div class="description">{{coupon_description}}</div>' +
    '</div>' +
    '</div>' +
    '<div class="expiration">Valid From: {{valid_from}} to {{valid_to}}</div>' +
    '</div>');

//Your Collection
var GroceryItems = Backbone.Collection.extend({

    model: GroceryItem,

    url: "data.json",

    parse: function (response) {
        return response.all_coupons;
    }

});

//This Represents all your views
var ItemsView = Backbone.View.extend({
    tagName: 'ul',
    el: '.where-this-is-supposed-to-go',
    initialize: function () {
        this.collection = new GroceryItems();
        //basically says only render when collection syncs
        this.listenTo(this.collection, 'sync', this.render);
    },

    render: function () {
        //got keep track of views for when you need close them (not important for now but you'll thank me later)

        this.itemViews = [];
        this.collection.each(function (m) {
            //we are making a new view for each model and passing it in as an option
            var itemView = new ItemView({
                model: m
            });

            //The view creates an element but it is not attached to DOM. We will attach it to the ItemsView's $el (which IS attached to the DOM)
            this.$el.append(itemView.$el);

            this.itemViews.push(itemView);
        }, this);
    }
});

var ItemView = Backbone.View.extend({
    template: itemTpl,
    tagName: 'li',
    className: 'lineItem',
    events: {
        'click': 'getModel'
    },
    initialize: function () {
       this.render();
    },
    getModel: function () {
        //it's already there. No IDs
        return this.model;
    },
    render: function () {
        this.$el.html(this.template(this.model.toJSON()));
    }
});
Toli
  • 5,547
  • 8
  • 36
  • 56
  • Thanks for the help! I'm intrigued by not having to use the ID. I want to work through both solutions to better understand everything. I'll report back once I've tested these. – Trevor Jul 11 '14 at 20:16
  • As I read this more closely I see you have a view for each individual li and then a view for all of the them. Would you recommend using Marionette? I've came across it in my readings and it seems to do what you are doing with ItemView and CompositeViews? I was just trying to avoid learning another library. – Trevor Jul 11 '14 at 20:23
  • @user3298335 Marionette implements this because it's a best practice :P But actually I'm anti-Marionette (was forced to use it against my will for ~ 1 month) because besides a few best practices (which I DO agree with) it forces a lot of architectural decision I DON'T agree with. Best way as you said is experiment with all solutions and see WHY something is better for yourself. – Toli Jul 12 '14 at 15:07
  • Hey so your solution worked great. I have one follow up question, with a small piece that isn't working. The 'ul' tag isn't getting populated when rendered. I get a long list of 'li' but they are not wrapped in the 'ul' so the styling is all off. Any idea why? I updated my code in the orginial question. Also, for whatever reason I still had to use fetch() to get this to work, and I passed in the new itemView that was fetched. Only way I could get it all to work. Thanks a bunch for your help! – Trevor Jul 14 '14 at 13:58
  • @user3298335 hmm can you do me a favor and post on jsfiddle? That way I can get a better idea of what's going on. As long as the itemsView (parent) has `ul` as `tagName` you should be fine.... Oh yeah and you still need to call `fetch`! Sorry I forgot (in my code models auto-fetch) – Toli Jul 15 '14 at 19:01
  • AH I know why :) Is #container a `
      `?
    – Toli Jul 15 '14 at 19:10
  • 1
    oh and also this is not related by don't list `GroceryItemsView` as a dependency for `GroceryItemView` (you may have weird problems because this is a CIRCULAR dependency). ALSO instead of saying ` var GroceryItemView = Backbone.View.extend({... return GroceryItemView` just `return Backbone.View.extend({...`. Same with the `GroceryItemsView` I would return the actual view (constructor) and do the `new GroceryItemsView` command in the view that's calling. It's best practice to make it modular (just in case `#container` is not on the page when require loads the script). – Toli Jul 15 '14 at 19:14
  • Btw I encourage you to keep posting questions. You are a very good case study of how people get started with Backbone the "wrong" way, and then have very complex code. One of my goals is to help make Backbone code a LOT less complex and outline best practices (like above) that save you a LOT of headaches. – Toli Jul 15 '14 at 19:16
  • Hey, thanks for all the responses. I would post it on fiddle but I've ended up changing a lot of the code already and I'm no longer using ul, in one template I'm using a table, and in the other its just divs. But the good news is I'm having no further issues with these new templates. I'll see if I can throw together a fiddle example just to show you. I'll have to play around with it first, haven't used ti before. Oh, and the #container was on a div originally. – Trevor Jul 18 '14 at 14:19
  • I tried to see if there was a way to message you on here but doesn't seem to exist. So I tried linked in as well but that wouldn't let me either. I was going to share with you what I have. – Trevor Jul 18 '14 at 14:31
  • @Trevor you can put a jsfiddle link in the body of your question and notify me by typing @ and my name. The reason there is no messaging on stackoverflow is to keep everything public knowledge (i.e. if i help you privately this answer helps just you and not the thousands of people with same issue) the reason you were having problems was because the #container tag for a list has to be set if its in the dom already (can't change div into a ul or table). – Toli Jul 19 '14 at 13:53