如何创建一个可重用的骨干/ backbone.marionette组件这样的datagrid?

时间:2012-08-23 06:10:14

标签: backbone.js marionette

我是Backbone和Backbone.Marionette的新手。我成功创建了一个带有一种数据网格的简单页面,允许我分页(第一页,上一页,下一页,最后一页),快速搜索(每按一次键时触发),选择页面上显示的项目数(5 ,10,全部,...)

现在我有一些工作,我试图改进它,并将这些功能作为一种可重用的组件,但我不知道确切的方法。我不知道如何开始完善已经完成的工作。

例如,我希望能够通过datagrid更改集合/模型管理,而无需重写所有内容。这是我不知道如何做到这一点,这可能是由于缺乏知识。因此,您的意见和建议将得到更多的欢迎和欢迎。

// JST and HAML Assets is used for the templating pre-compilation
Backbone.Marionette.Renderer.render = function(template, data) {
  if (!JST[template]) {
    throw "Template '" + template + "' not found!";
  }
  return JST[template](data);
};

window.MyApp = new Backbone.Marionette.Application();

MyApp.addRegions({
  content: ".content-box"
});

MyApp.Datagrid = (function() {
  var Datagrid, ItemPerPageView, Layout, PagerView, QuickSearchView, Theme, ThemeView, Themes, ThemesView;

  Datagrid = {};

  Layout = Backbone.Marionette.Layout.extend({
    template: "layouts/grid",
    regions: {
      grid: "#grid",
      quickSearch: "#quickSearch",
      itemPerPage: "#itemPerPage",
      pager: ".pager"
    }
  });

  Theme = Backbone.Model.extend();

  Themes = Backbone.ExtendedCollection.paginatedCollection.extend({
    url: "/themes",
    model: Theme,

    initialize: function() {
      var _this = this;

      MyApp.vent.on("quickSearch:term", function(term) {
        _this.quickSearch(term);
      });

      MyApp.vent.on("itemPerPage:count", function(count) {
        _this.perPage(count);
      });

      MyApp.vent.on("pager:previous", function() {
        _this.previous();
      });

      MyApp.vent.on("pager:next", function() {
        _this.next();
      });

      MyApp.vent.on("pager:first", function() {
        _this.first();
      });

      MyApp.vent.on("pager:last", function() {
        _this.last();
      });
    }
  });

  ThemeView = Backbone.Marionette.ItemView.extend({
    tagName: "tr",
    template: "theme",
    model: Theme,

    events: {
      "click span": "edit",
      "blur input": "save"
    },

    edit: function(event) {
      var id, span;
      id = this.model.get("id");
      span = $("span", this.el).hide();
      $("input", this.el).show().focus().val(span.text());
    },

    save: function(event) {
      var id, input, span;
      id = this.model.get("id");
      span = $("span", this.el).show();
      input = $("input", this.el).hide();
      if (this.model.get("name") !== input.val()) {
        this.model.set("name", input.val());
        this.model.save();
      }
      span.text(this.model.get("name"));
    }
  });

  ThemesView = Backbone.Marionette.CompositeView.extend({
    template: "index",
    model: Theme,
    itemView: ThemeView,
    collection: Themes,
    itemViewContainer: "#themes",

    serializeData: function() {
      return this.data;
    }
  });

  QuickSearchView = Backbone.Marionette.View.extend({
    el: "#quickSearch",

    events: {
      "keyup input": "search"
    },

    search: function(event) {
      var searchTerm;
      searchTerm = this.$("input").val().trim();
      MyApp.vent.trigger("quickSearch:term", searchTerm);
    }
  });

  ItemPerPageView = Backbone.Marionette.View.extend({
    el: "#itemPerPage",

    events: {
      "change select": "count"
    },

    count: function(event) {
      var count;
      count = this.$("select").val();
      MyApp.vent.trigger("itemPerPage:count", count);
    }
  });

  PagerView = Backbone.Marionette.View.extend({
    el: ".pager",

    events: {
      "click #next": "next",
      "click #previous": "previous",
      "click #first": "first",
      "click #last": "last"
    },

    first: function(event) {
      MyApp.vent.trigger("pager:first");
    },

    last: function(event) {
      MyApp.vent.trigger("pager:last");
    },

    next: function(event) {
      MyApp.vent.trigger("pager:next");
    },

    previous: function(event) {
      MyApp.vent.trigger("pager:previous");
    }
  });

  Datagrid.initializeLayout = function() {
    var collection;

    Datagrid.layout = new Layout();

    Datagrid.layout.on("show", function() {
      MyApp.vent.trigger("layout:rendered");
    });

    MyApp.content.show(Datagrid.layout);

    collection = new Themes();
    collection.fetch();

    collection.on("reset", function() {
      return Datagrid.layout.grid.show(new ThemesView({
        collection: collection
      }));
    });
  };

  MyApp.vent.on("layout:rendered", function() {
    var itemPerPageView, pagerView, quickSearchView;

    quickSearchView = new QuickSearchView();
    Datagrid.layout.quickSearch.attachView(quickSearchView);

    itemPerPageView = new ItemPerPageView();
    Datagrid.layout.itemPerPage.attachView(itemPerPageView);

    pagerView = new PagerView();
    Datagrid.layout.pager.attachView(pagerView);
  });

  return Datagrid;
})();

MyApp.addInitializer(function() {
  MyApp.Datagrid.initializeLayout();
});

$(document).ready(function() {
  return MyApp.start();
});

修改1:

根据给出的答案和我自己的想法,我写了一个解决方案的初稿。我没有成功编写一个真正的可重用组件,但我有一个解决方案来整合我的代码。有些部分需要重构和改进。我想在稍后的重构中解决一些陷阱问题。

要添加一些上下文,应用程序将使用Rails作为后端编写。所以有我的javascript文件夹结构

assets
|--javascripts
   |--application.js
   |--admin
      |--admin.js
      |--admin.layout.js
      |--subthemes
         |--admin.subtheme.controller.js
         |--admin.subtheme.view.js
      |--themes
         |--admin.theme.controller.js
         |--admin.theme.view.js
|--templates
   |--admin
      |--subthemes
         |--index.hamlc
         |--subtheme.hamlc
      |--themes
         |--index.hamlc
         |--theme.hamlc
   |--layouts
      |--grid.hamlc

首先,application.js启动。 Rails 3.2中的资产管道将按预期准备依赖项:

//= require underscore
//= require backbone
//= require backbone.marionette
//= require_tree ./lib/backbone
//= require hamlcoffee
//= require i18n
//= require i18n/translations
//= require_tree ../templates/
//= require_tree ./admin
//= require_tree ./admin/theme
//= require_tree ./admin/subtheme

I18n.defaultLocale = "en";


Backbone.Marionette.Renderer.render = function(template, data) {
  if (!JST[template]) {
    throw "Template '" + template + "' not found!";
  }
  return JST[template](data);
};

$(document).ready(function() {
  return MyApp.start();
});

现在,我们可以准备管理部分开始:

var AdminRouter, TempView;

// Create the application for admin part
MyApp.Admin = new Backbone.Marionette.Application();

// Define a router to handle the grid collection type change
AdminRouter = Backbone.Marionette.AppRouter.extend({
  initialize: function() {
    var _this = this;

    // Route quite generic to easily change the data in the grid
    this.route(/^admin\/(.*?)$/, "changeCollection");

    // Manage event to handle the navigation on client side
    MyApp.Admin.vent.on("admin:navigate", function(link) {
      _this.navigate(link, {
        trigger: true
      });
    });
  },

  // Trigger an event to change the collection if one exist for the URL
  changeCollection: function(collectionName) {
    MyApp.Admin.vent.trigger("grid:collection:change", collectionName);
  }
});

// Side menu that allows changing the collection in the data grid
SideMenuView = Backbone.Marionette.View.extend({
  el: ".side-menu",

  events: {
    "click a": "handleClick"
  },

  // Prevent the normal behavior on the link click
  handleClick: function(event) {
    event.preventDefault();
    MyApp.Admin.vent.trigger("admin:navigate", $(event.target).attr("href"));
  }
});

// Add the initializer to the main application to prepare the admin part (grid)
MyApp.addInitializer(function() {
  new SideMenuView();
  new AdminRouter();
  Backbone.history.start({
    pushState: true
  });
  MyApp.Admin.start();
});

然后我们可以定义datagrid部分:

// This the grid layout module in the admin namespace
MyApp.Admin.module("GridLayout", function(GridLayout, Admin, Backbone, Marionette, $, _) {
  var ItemPageSelectorView, Layout, PagerView, QuickSearchView;

  // The quick search view handle the related fields to do the quick search
  QuickSearchView = Backbone.Marionette.View.extend({
    el: ".gridQuickSearch",

    events: {
      "keyup input": "search"
    },

    // Get the field content and trigger an event with it
    search: function(event) {
      var searchTerm;
      searchTerm = $(event.target).val().trim();
      $("input", this.$el).val(searchTerm);
      Admin.vent.trigger("grid:quickSearch:term", searchTerm);
    }
  });

  // The item page selecto handle the choice of how many rows should be displayed per page
  ItemPageSelectorView = Backbone.Marionette.View.extend({
    el: ".gridItemPageSelector",

    events: {
      "change select": "count"
    },

    // Get the number of items per page that should be displayed
    count: function(event) {
      var count;
      count = $(event.target).val();
      $("select", this.$el).val(count);
      Admin.vent.trigger("grid:itemPageSelector:count", count);
    }
  });

  // The pager view manage the view components to change the page shown in the data grid
  PagerView = Backbone.Marionette.View.extend({
    el: ".gridPager",

    events: {
      "click #next": "next",
      "click #previous": "previous",
      "click #first": "first",
      "click #last": "last",
      "click #page": "page"
    },

    //
    // The following functions triggers events to go to the right pages
    //
    first: function(event) {
      Admin.vent.trigger("grid:pager:first");
    },

    previous: function(event) {
      Admin.vent.trigger("grid:pager:previous");
    },

    page: function(event) {
      Admin.vent.trigger("grid:pager:page");
    },

    next: function(event) {
      Admin.vent.trigger("grid:pager:next");
    },

    last: function(event) {
      Admin.vent.trigger("grid:pager:last");
    }
  });

  // The grid layout with the regions to display the different part of the data grid
  Layout = Backbone.Marionette.Layout.extend({
    template: "layouts/grid",

    regions: {
      gridTable: "#gridTable",
      gridQuickSearch: ".gridQuickSearch",
      gridItemPageSelector: ".gridItemPageSelector",
      gridPager: ".gridPager"
    }
  });

  // Once the layout is rendered, the different views are attached to the right regions
  Admin.vent.on("grid:layout:rendered", function() {
    var itemPageSelectorView, pagerView, quickSearchView;

    quickSearchView = new QuickSearchView();
    Admin.gridLayout.gridQuickSearch.attachView(quickSearchView);

    itemPageSelectorView = new ItemPageSelectorView();
    Admin.gridLayout.gridItemPageSelector.attachView(itemPageSelectorView);

    pagerView = new PagerView();
    Admin.gridLayout.gridPager.attachView(pagerView);
  });

  // Initializer to do at the application start
  GridLayout.addInitializer(function() {
    Admin.addRegions({
      content: ".content-box"
    });

    Admin.gridLayout = new Layout();

    // Trigger the rendered event when the grid layout is shown
    Admin.gridLayout.on("show", function() {
      Admin.vent.trigger("grid:layout:rendered");
    });

    // Manage the collection data change
    Admin.vent.on("grid:collection:change", function(collectionName) {
      // Close the previous view in the grid table region
      Admin.gridLayout.gridTable.close();

      // Trigger an event to fetch the collection
      Admin.vent.trigger("" + collectionName + ":collection:fetch");

      // Show the grid layout if not already done
      if (!this.shown) {
        this.shown = true;
        Admin.content.show(Admin.gridLayout);
      }
    });
  });

  return GridLayout;
});

我们完成了结构代码。现在我们可以去一个控制器。例如,ThemeController:

MyApp.Admin.module("ThemeController", function(ThemeController, Admin, Backbone, Marionette, $, _) {
  // Define the model to use in the collection
  ThemeController.Theme = Backbone.Model.extend();

  // Define the collection with the related url on the server. The collection extends a paginated collection that has the methods to manage the quick search and the pagination
  ThemeController.Themes = Backbone.ExtendedCollection.paginatedCollection.extend({
    url: "/admin/themes",

    model: ThemeController.Theme,

    initialize: function() {
      var _this = this;

      //
      // The following functions handle the events for the quick search and pagination
      //

      Admin.vent.on("grid:quickSearch:term", function(term) {
        _this.quickSearch(term);
      });

      Admin.vent.on("grid:itemPageSelector:count", function(count) {
        _this.perPage(count);
      });

      Admin.vent.on("grid:pager:previous", function() {
        _this.previous();
      });

      Admin.vent.on("grid:pager:next", function() {
        _this.next();
      });

      Admin.vent.on("grid:pager:first", function() {
        _this.first();
      });

      return MyApp.Admin.vent.on("grid:collection:fetched", function() {
        Admin.gridLayout.gridTable.show(new Admin.ThemeView.Table({
          collection: _this
        }));
      });
    }
  });

  // At the application initilization, we need to be sure this controller can 
  // handle the event to fetch the data from the server
  Admin.addInitializer(function() {
    Admin.vent.on("themes:collection:fetch", function() {
      ThemeController.themes = new ThemeController.Themes();

      // Once the data are fetched from the server, trigger an event to display them
      ThemeController.themes.fetch({
        success: function() {
          Admin.vent.trigger("grid:collection:fetched");
        }
      });
    });
  });
});

最后是前一个控制器的观点:

MyApp.Admin.module("ThemeView", function(ThemeView, Admin, Backbone, Marionette, $, _) {
  // The view to show one item in a row of the data grid
  ThemeView.Item = Backbone.Marionette.ItemView.extend({
    tagName: "tr",
    template: "admin/themes/theme",
    model: Admin.ThemeController.Theme
  });

  // The view to show the collection of item
  ThemeView.Table = Backbone.Marionette.CompositeView.extend({
    template: "admin/themes/index",
    model: Admin.ThemeController.Theme,
    itemView: ThemeView.Item,
    collection: Admin.ThemeController.Themes,
    itemViewContainer: "#themes",

    // ! I was force to add this to have data in the original format that is used by my templates !
    serializeData: function() {
      return this.data;
    }
  });
});

备注:子主题控制器和视图文件包含完全相同类型的代码。只有模板和类型的东西不同。

通过Rails资产管道编译的HAML中的网格布局如下所示:

.gridPager
  %button#first= "<<"
  %button#previous= "<"
  %button#next= ">"
  %button#last= ">>"

%span.gridItemPageSelector= "Item per page"
  %select
    %option= 5
    %option= 10
    %option{"value" => -1}= "All"

%span.gridQuickSearch= "Quick search:"
  %input#gridSearchTerm{"type" => "text"}

#gridTable

%span.gridItemPageSelector= "Item per page"
  %select
    %option= 5
    %option= 10
    %option{"value" => -1}= "All"

%span.gridQuickSearch= "Quick search:"
  %input#gridSearchTerm{"type" => "text"}

.gridPager
  %button#first= "<<"
  %button#previous= "<"
  %button#next= ">"
  %button#last= ">>"

正如你所看到的,有很多重复。我想在网格的顶部和底部快速搜索和分页。目前,最简单的方法是复制代码。我将在以后找到如何做到的时候改变它。

显示主题的表格模板:

%table.table.table-striped
  %thead
    %tr
      %th= "Id"
      %th= "Name"
  %tbody#themes

非常简单,没什么好说的。此时,标题是硬编码的!

最后,项目视图模板显示主题:

%td= this.id
%td= this.name

这个模板非常简单。

我处于一种非常好的工作状态。例如,当我点击其他链接更改显示的集合时,快速搜索字段和类似的东西不会重新初始化。为此,我想添加一种状态管理来跟踪收集状态,当回到已经显示的集合时,我想像以前一样显示它。

我确信我的解决方案并不完美,可以进行大量重构。我也可能犯了很多“新手”的错误。所以随意挑战我的主张。我尝试学习并改进我的解决方案,并希望它能帮助某人做类似的事情。

2 个答案:

答案 0 :(得分:2)

嗯,我不是一个大专家,但我就是这样做的,使用Marionette和Requirejs:

a)我创建了一个通用的Grid Layout wiew,由我的approuter调用,带有一些参数,如collection,cols config(我用每个循环渲染表头)和一个行视图:

showUsers: function(){

    require(['views/GridGen','collections/user_collection'], function(Grid, UserCollection){

        var Users = new UserCollection();

        App.grid = new Grid({collection: Users ,
                             rowView: 'rowUser',
                             cols_config: App.tables.users});

        App.page.show(App.grid);

    });
},

b)在我的网格布局中,我渲染了等待onShow事件的各个部分:

var Grid = Backbone.Marionette.Layout.extend({

regions: {
            top_controls: "#top_controls",
            table_view: "#table_view",
            pagination_controls: "#pagination_controls",
            bottom_controls: "#bottom_controls",
         },

onShow: function(){              
    this.renderTable(this.collection);
},

renderTable: function(collection){

    collection.fetch({success:function(){

          require(['views/'+self.options.rowView+'.js'],function(iView){

                   var vista = new View({collection: collection, itemView: iView, thead: self.options.cols_config});

                   App.grid.table_view.show(vista);

                   self.renderPagination(collection);

                   collection.pager();
               });
          }});
}

c)我的通用表视图将cols和itemView呈现为如下参数:

var View = Backbone.Marionette.CompositeView.extend({

initialize: function(){
        this.itemView = this.options.itemView;           
},
serializeData: function(){
        var data = Backbone.Marionette.ItemView.prototype.serializeData.apply(this, arguments);
        data.thead = this.options.thead;
        return data;
},
appendHtml: function(collectionView, itemView, index){
        collectionView.$("tbody").append(itemView.el);
},

这只是一个普遍的想法,我不认为这是最好的方法,但我还没有找到更好的解决方案,希望至少给你一些提示:)

答案 1 :(得分:1)

我强烈推荐可扩展且可重复使用的backgrid component

只有版本0.2.6 - 但很好的跟随和漂亮的光滑