2

I have a collection of items. I would like to keep track of the current selection. When the user clicks on a different item in the collection, I want to indicate that the item is selected and display the details of the selected item. Think of this as a list with a detail view (like a typical email client).

Example of a master-detail layout (source): Example of a master-detail layout

I currently have something like this (written in CoffeeScript, templates use haml-coffee):

class Collections.Items extends Backbone.Collection
  model: Models.Item

  setCurrentSelection: (id)->
    # what to do here? Is this even the right way to do it?

  getCurrentSelection: ->
    # what to do here? Is this even the right way to do it?

class Views.ItemsPage extends Backbone.View

  list_template: JST['items/list']
  details_template: JST['items/details']

  events:
    'click .item': 'updateSelection'

  initialize: (options)->
    @collection = options.collection

  render: ->
    $('#items_list').html(@list_template(collection: @collection.toJSON())) # not sure if this is how to render a collection
    $('#item_details').html(@details_template(item: @collection.currentSelection().toJSON())) # how to implement currentSelection?
    @

  updateSelection: (event)->
    event.preventDefault()
    item_id = $(event.currentTarget).data('id')
    # mark the item as selected
    # re-render using the new selection

# templates/items/list.hamlc
%ul
  - for item in @collection
    %li{data:{id: item.id}, class: ('selected' if item.selected?)} # TODO: How to check if selected?
      = item.name

# templates/items/details.hamlc
%h2= @item.name
Andrew
  • 227,796
  • 193
  • 515
  • 708

2 Answers2

1

I'm not sure if I'm following you (my CoffeeScript is a bit rusty), but I think what you're trying to do is set a selected property on the appropriate model in your updateSelection method, and then re-render your view.

In other words:

updateSelection: (event)->
    event.preventDefault()
    item_id = $(event.currentTarget).data('id')
    model = this.collection.get(item_id) # get the model to select
    model.selected = true # mark the item as selected
    this.render() # re-render using the new selection
machineghost
  • 33,529
  • 30
  • 159
  • 234
  • Does this set the property on the model globally? For example, if I fetch it again with `this.collection.get(item_id)`, will the model still have the `selected` property? But the more important question is "is this the right way to do it?" Or is there a better, cleaner approach that uses Backbone the way it was intended? – Andrew Oct 06 '14 at 23:18
  • It doesn't; if you want the data to persist you should use an *attribute* instead (eg. `model.set('selected', true);`) and then `save` the model at some point. As for the "right way", whenever you have data you want to preserve on the server in Backbone, the correct way to preserve it is to save it as a `Model` attribute (so yes, it is the right way). If this was just a temporary selection that wouldn't be preserved, then a property (as I originally suggested) would be the better way. – machineghost Oct 06 '14 at 23:33
  • Sorry, I meant to say that "selected" is only a client-side state. It does not need to be persisted on the server side, but across Backbone routes. I just meant there may be a better way of keeping track of state that I haven't thought of. – Andrew Oct 07 '14 at 05:44
  • If you just need it to be persisted across routes you can use a simple variable. For instance `Views.ItemsPage.currentSelection = item_id`. If you want it to also be persisted when the user refreshes the page though, that won't work, and instead you'll have to use cookies or local storage. – machineghost Oct 07 '14 at 16:53
1

even saying "my CoffeeScript is a bit rusty" is too much for me. But i'll still attempt to explain as best as i can in js.

First the backbone way is to keep models as a representation of a REST resource document. (server side - persisted data).

Client side presentation logic should stick to views. to remember which list item is visible in in the details part is job of the that specific view. initiating change request for details view model is job of the list of items.

the ideal way is to have two separate views for list and details. (you can also go a bit more ahead and have a view for every item in the list view.

parent view

var PageView = Backbone.View.extend({
        initialize: function() {
            //initialize child views
            this.list = new ItemListView({
                collection : this.collection   //pass the collection to the list view
            });
            this.details = new ItemDetailView({
                model : this.collection.at(1)   //pass the first model for initial view
            });

            //handle selection change from list view and replace details view
            this.list.on('itemSelect', function(selectedModel) {
                this.details.remove();
                this.details = new ItemDetailView({
                    model : selectedModel
                });
                this.renderDetails();
            });
        },

        render: function() {
            this.$el.html(this.template); // or this.$el.empty() if you have no template
            this.renderList();
            this.renderDetails();
        },

        renderList : function(){
            this.$('#items_list').append(this.list.$el);  //or any other jquery way to insert 
            this.list.render();
        },

        renderDetails : function(){
            this.$('#item_details').append(this.details.$el);  //or any other jquery way to insert 
            this.details.render();
        }
    });

list view

var ItemListView = Backbone.View.extend({
    events : {
        'click .item': 'updateSelection'
    },
    render: function() {
        this.$el.html(this.template);
        this.delegateEvents();  //this is important
    }
    updateSelection : function(){
        var selectedModel;
        // a mechanism to get the selected model here - can be same as yours with getting id from data attribute
        // or you can have a child view setup for each model in the collection. which will trigger an event on click.
        // such event will be first captured by the collection view and thn retriggerd for page view to listen.
        this.trigger('itemSelect', selectedModel);
    }
});

details view

var ItemDetailView = Backbone.View.extend({
    render: function() {
        this.$el.html(this.template);
        this.delegateEvents();  //this is important
    }
});

This won't persist the state through routes if you don't reuse your views. in that case you need to have a global state/event saving mechanism. somthing like following -

window.AppState = {};
_.extend(window.AppState, Backbone.Events);

//now your PageView initilize method becomes something like this -
initialize: function() {
    //initialize child views
    this.list = new ItemListView({
        collection : this.collection   //pass the collection to the list view
    });
    var firstModel;
    if(window.AppState.SelectedModelId) { 
        firstModel = this.collection.get(window.AppState.SelectedModelId);
    } else {
        firstModel = this.collection.at(1);
    }
    this.details = new ItemDetailView({
        model : firstModel   //pass the first model for initial view
    });

    //handle selection change from list view and replace details view
    this.list.on('itemSelect', function(selectedModel) {
        window.AppState.SelectedModelId = selectedModel.id;
        this.details.remove();
        this.details = new ItemDetailView({
            model : selectedModel
        });
        this.renderDetails();
    });
}

EDIT
Handling selected class (highlight) in list view . see comments for reference.

list view template -

<ul>
  <% _.each(collection, function(item, index){ %>
    <li data-id='<%= item.id %>'><%= item.name %></li>
  <% }); %>
</ul>

inside list view add following method -

changeSelectedHighlight : function(id){
  this.$(li).removeClass('selected');
  this.$("[data-id='" + id + "']").addClass('selected');
}

simply call this method from updateSelection method and during PageView initialize.

this.list.changeSelectedHighlight(firstModel.id);
Mohit
  • 2,239
  • 19
  • 30
  • I like where this is going. Can you elaborate on the purpose of `delegateEvents()` and why it is important? – Andrew Oct 07 '14 at 18:44
  • Would it be a bad idea to reuse the `ItemDetailView` by setting the `model` property with the new model, then re-render? Or is it better to remove, create a new instance, and render the new instance? – Andrew Oct 07 '14 at 18:48
  • Last question: what would a template look like for adding a class of `selected` to the item? – Andrew Oct 07 '14 at 19:46
  • delegateEvents is important because once the view and its releated events are removed with the remove() call and a new view is inserted. we need to rebind the events on the child view. for example. your collection got some update. and you want to re-render list view. delegateEvents will rebind all the events, thus including new models to the event chain. http://stackoverflow.com/questions/11073877/delegateevents-in-backbone-js – Mohit Oct 08 '14 at 09:29
  • you can change model property and re-render. but the event chain will create problem. for the code above its ok. but if you want to start listing to model events in the detail view. you'll still be listing to old model even after re-render and model property change. – Mohit Oct 08 '14 at 09:31
  • "adding a class of selected to the item" by this do you mean highlighting the selected item in the listView. I am adding a edit in the answer with underscore templates. I don't know haml much. – Mohit Oct 08 '14 at 09:37
  • Yes, the `.addClass('selected')` would work. I was wondering how to do this at the template level. For example, if an item was pre-selected, shouldn't the template be responsible for adding the 'selected' class? – Andrew Oct 08 '14 at 21:10
  • for that you can pass the firstModel.id as one of the options to the ListView, and during render you can JSONify that along with collection data. and check if it exists in the data in the template and apply. The essential point is to make selected information available to template from view. its not a good idea to make it a property of model. let the model represent a REST resource. and either set property on View or pass as options in initialization. use this information during template rendering. – Mohit Oct 09 '14 at 06:32