admin管理员组

文章数量:1314066

This is a simple problem for beginner but so far I haven't seen a solution that fits my need. Basically I got a simple menu with ul and li. There are 2 requirements:

Req1: When click on one, the li will get new class .active.

Req2: Menu items are dynamic, meaning I should be able to add or remove any menu item (by using some other button).

There are 2 ways to do this:

Method 1: Tranversing for each MenuView as MenuItem

I have a MenuView with something like this

el:  $('li'),

events: {
  "click" : "highlight"
},

highlight: function(e) {
  thisParent = $(e.target).parent();
  thisParent.siblings('.active').removeClass('active');
  thisParent.addClass('active');
},

Pro: Easy. This is what I have now.

Con: Dependency on the html structure. What if it's changed to div instead with many layers.

Method 2: One View for the MenuCollection

Create a MenuItemCollection and use MenuView for that collection instead. The el for MenuView will be ul (instead of li). The HTML will look like this with separate id:

<ul>
    <li id="leftmenu-one">one</li>
    <li id="leftmenu-two">two</li>
    <li id="leftmenu-three">three</li>
</ul>

Then when a click event is detected, do 2 things:

2a. Remove all .active class in ul li

2b. Add .active class to the e.target DOM

Pro: Decoupling html design

Con: Little more code.

QUESTION: I think most people would say Method 1 is bad. But is there method 3, 4, 5... that are better? How to handle the adding of new menu item?

This is a simple problem for beginner but so far I haven't seen a solution that fits my need. Basically I got a simple menu with ul and li. There are 2 requirements:

Req1: When click on one, the li will get new class .active.

Req2: Menu items are dynamic, meaning I should be able to add or remove any menu item (by using some other button).

There are 2 ways to do this:

Method 1: Tranversing for each MenuView as MenuItem

I have a MenuView with something like this

el:  $('li'),

events: {
  "click" : "highlight"
},

highlight: function(e) {
  thisParent = $(e.target).parent();
  thisParent.siblings('.active').removeClass('active');
  thisParent.addClass('active');
},

Pro: Easy. This is what I have now.

Con: Dependency on the html structure. What if it's changed to div instead with many layers.

Method 2: One View for the MenuCollection

Create a MenuItemCollection and use MenuView for that collection instead. The el for MenuView will be ul (instead of li). The HTML will look like this with separate id:

<ul>
    <li id="leftmenu-one">one</li>
    <li id="leftmenu-two">two</li>
    <li id="leftmenu-three">three</li>
</ul>

Then when a click event is detected, do 2 things:

2a. Remove all .active class in ul li

2b. Add .active class to the e.target DOM

Pro: Decoupling html design

Con: Little more code.

QUESTION: I think most people would say Method 1 is bad. But is there method 3, 4, 5... that are better? How to handle the adding of new menu item?

Share Improve this question edited Oct 16, 2012 at 6:10 HP. asked Oct 16, 2012 at 5:39 HP.HP. 19.9k56 gold badges159 silver badges258 bronze badges 6
  • Your code is a bit too reliant on the HTML structure. You shouldn't be doing that much DOM traversal in Backbone, because each View contains it's own HTML references. Instead of traversing the DOM, isolate the correct View and then have it handle the traversal on a smaller scale. A good way to do this is with Backbone Events. – Cory Danielson Commented Oct 16, 2012 at 9:27
  • @CoryDanielson Right I knew it and was checking other creative alternatives. Some reason your answer below was deleted. Can you repost? I'd love to keep it to study later. – HP. Commented Oct 17, 2012 at 8:30
  • 1 Alright, I undeleted it. The only difference between mine and the other answer is that I don't have active as part of the model, because I don't really see that as business data that would be saved in any database.. it'd be a wasted column.. it's more of a UI/view thing. Because of that, I don't have change:active, and threw a custom event instead. – Cory Danielson Commented Oct 17, 2012 at 17:20
  • 1 I also have Req2 in there (Menu items are dynamic, meaning I should be able to add or remove any menu item (by using some other button).) – Cory Danielson Commented Oct 17, 2012 at 17:21
  • 1 The other way around. Every menuItemView is in the menuView. Memory isn't an issue, everything is by reference – Cory Danielson Commented Oct 18, 2012 at 4:52
 |  Show 1 more ment

3 Answers 3

Reset to default 8

Create a menu item model

    var MenuItem = Backbone.Model.extend({
        title: 'Default Title',
        isSelected: false
    });

and items collection, that would listen to any model selection change event

   var MenuItemCollection = Backbone.Collection.extend({
        model: MenuItem,

        initialize: function() {
            this.on('change:isSelected', this.onSelectedChanged, this);
        },

        onSelectedChanged: function(model) {
            this.each(function(model) {
                if (model.get('isSelected') === true && !model.hasChanged('isSelected')) {
                    model.set({isSelected: false});
                }
            });
        }
    });

After that create a view each for menu item

   var MenuItemView = Backbone.View.extend({
        tagName:  'li',
        events: {
          'click' : 'highlight'
        },

        initialize: function() {
            _.bindAll(this);
            this.model.on('change:isSelected', this.onSelectedChanged);
        },

        render: function() {
            this.$el.text(this.model.get('title'));
            return this;
        },

        onSelectedChanged: function() {
            if (this.model.get('isSelected') === true) {
                this.$el.addClass('active');
            }
            else {
                this.$el.removeClass('active');
            }
        },

        highlight: function() {
            this.model.set({isSelected: true});
        }
    });

and menu itself like

   var MenuView = Backbone.View.extend({
        tagName:  'ul',

        initialize: function() {
            _.bindAll(this);
        },

        render: function() {
            this.collection.each(function(model) {
                var item = new MenuItemView({model: model});
                this.$el.append(item.render().el);
            }, this);

            return this;
        }
    });

Full working js fiddle with ments at http://jsfiddle/Kf3SS/

JSFiddle Demo w/ code ments

There's a bit of DOM traversal in your views that you most-likely want to avoid... because this example is so small, it's not going to ruin the user experience, but it's a best practice and all that good stuff.

I think this should be a good starting point/example for you.

Variable Setup

var MenuView
,   MenuItemView
,   MenuItemModel
,   menu          //MenuView Instance
,   menuItem      //MenuItemView Instance
;

MenuItemModel

MenuItemModel = Backbone.Model.extend({
    'defaults': {
        'url': undefined
    ,   'text': undefined
    }
});

MenuView (<ul>)

MenuView = Backbone.View.extend({
    'tagName': 'ul'
,   'id': 'MenuView'
,   'menuItems': []
,   'activeButton': undefined
,   'initialize': function ( menuObjJSON, parent ) {
        var menuItem;

        for ( menuItemIndex in menuObjJSON ) {
            this.addMenuItem( new MenuItemModel(menuObjJSON[menuItemIndex]) )
        }
        this.render(parent);
    }
,   'render': function ( parent ) {
        $(parent).append(this.$el);
    }
,   'addMenuItem': function ( model ) {
    var menuItem = new MenuItemView({
            'model': model, 
            'parentElement': this.$el
        });
        menuItem.on('changeActive', this.setActiveButton, this);
        this.menuItems.push( menuItem );
        return menuItem;
}
,   'removeMenuItem': function ( identifier ) {
        var i, menuItem, length = this.menuItems.length, menuItemsCopy;
        for (i = 0; i < length; i++) {
            if ( this.menuItems[i] ) {
                menuItemView = this.menuItems[i];
                if ( menuItemView.model.get('text').toLowerCase() === identifier.toLowerCase() 
                     || menuItemView.model.get('url').toLowerCase() === identifier.toLowerCase() ) 
                {
                    menuItemView.destroy();
                    debugger;
                    menuItemsEnd = this.menuItems.slice(i+1, length);
                    this.menuItems = [].concat(this.menuItems.slice(0,i), menuItemsEnd);
                    return true;
                }
            }
        }
        return false; //if menu item not found
    }
,   'setActiveButton': function ( activeMenuItem ) {
        if ( this.activeButton ) {
            this.activeButton.removeHighlight();
        }
        this.activeButton = activeMenuItem;    
    }
});

MenuItemView (<li>)

MenuItemView = Backbone.View.extend({
    'tagName': 'li'
,   'className': 'menuItem'
,   'events': { 
        'click a': 'highlight'
    }
,    'initialize': function ( options ) {
        this.render(options.model, options.parentElement);
    }
,   'render': function ( model, parentElement ) {
        this.$el.append("<a href='" + model.get('url')+ "'>" + model.get('text') + "</a>");
        parentElement.append(this.$el);
    }
,   'highlight': function ( event ) {
        if ( !this.$el.hasClass('active') ) {
            this.trigger('changeActive', this);
            this.$el.addClass('active');
        }
    }
,   'removeHighlight': function () {
        this.$el.removeClass('active');
    }
,   'destroy': function () {
        this.unbind('click a');
        this.remove(); //unbind from DOM, remove DOM events
    }
});

Menu Instantiation

menu = new MenuView([
    {'url': '#home',    'text': 'Home'}
,   {'url': '#catpics', 'text': 'Cat Pics'}
,   {'url': '#dogpics', 'text': 'Dog Pics'}
,   {'url': '#about',   'text': 'About Us'}
], $('body'));

Adding New Menu Items. (not the most practical example, but a starting point)

setTimeout(function(){
    menu.addMenuItem({'url': '#contact', 'text': 'Contact Us'});
}, 1000);

setTimeout(function(){
    menu.addMenuItem({'url': '#Login', 'text': 'Log In'});
}, 2000);

setTimeout(function(){
    menu.addMenuItem({'url': '#W3bm4573r', 'text': 'W3bm4573r'});
}, 3000);

setTimeout(function(){
    menu.addMenuItem({'url': '#something', 'text': 'Something'});
}, 4000);

Removing Menu Items (not the most practical example, but a starting point)

setTimeout(function(){
    menu.removeMenuItem('#contact');
}, 5000);

setTimeout(function(){
    menu.removeMenuItem('about us');
}, 5000);

setTimeout(function(){
    menu.removeMenuItem('#SOMETHING');
}, 5000);

I hope this may be helpful to you. use this jquery

$("ul li").click(function() {
$("ul li").removeClass("active");
$(this).addClass("active");
});

Demo Link

本文标签: javascriptClick menu item to highlight in Backbone ViewStack Overflow