我对 Backbone 和 Backbone.Marionette 很陌生。我成功地创建了一个带有数据网格的简单页面,它允许我分页(第一页、上一页、下一页、最后一页)、快速搜索(每次按下键时触发)、选择页面上显示的项目数(5 , 10, 所有, ...)
现在我有了一些工作,我试图改进它并将这些功能作为一种可重用的组件,但我不知道确切的方法。我不知道如何开始细化已经完成的工作。
例如,我希望能够更改由数据网格管理的集合/模型,而无需重写所有内容。这是我不确定如何做到这一点的地方,这可能是由于缺乏知识。因此,您的意见和建议将不胜感激和欢迎。
// 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;
});
我们完成了结构代码。现在我们可以转到其中一个控制器。例如,主题控制器:
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
这个模板非常简单。
我的情况很好。例如,当我单击其他链接以更改显示的集合时,快速搜索字段和类似的东西不会重新初始化。为此,我想添加一种状态管理来跟踪集合状态,当回到已经显示的集合时,我想像以前一样显示它。
我确信我的解决方案并不完美,可以进行很多重构。我也可能犯了很多“新手”错误。所以请随意挑战我的提议。我尝试学习和改进我的解决方案,并希望它能帮助某人做类似的事情。