backgrid
Version:
Backgrid.js is a set of components for building semantic and easily stylable data grid widgets with Backbone.
579 lines (483 loc) • 17.8 kB
JavaScript
/*
backgrid
http://github.com/cloudflare/backgrid
Copyright (c) 2013-present Cloudflare, Inc. and contributors
Licensed under the MIT license.
*/
describe("A Body", function () {
var col;
var body;
beforeEach(function () {
col = new Backbone.Collection([{id: 2}, {id: 1}, {id: 3}]);
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
});
it("renders table rows using the given column definitions and collection", function () {
expect(body.el.tagName).toBe("TBODY");
var $trs = body.$el.children();
expect($trs.length).toBe(3);
expect($(body.el).html().toLowerCase().replace(/\s*</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">2</td></tr>' +
'<tr><td class="integer-cell editable sortable renderable id">1</td></tr>' +
'<tr><td class="integer-cell editable sortable renderable id">3</td></tr>');
});
it("will render a new row if a new model is added to its collection", function () {
body.collection.add({
id: 4
});
var $trs = body.$el.children();
expect($trs.length).toBe(4);
expect($("<div>").append($trs.eq(3).clone()).html().toLowerCase().replace(/\s*</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">4</td></tr>');
body.collection.add({
id: 5
}, {at: 1});
$trs = body.$el.children();
expect($trs.length).toBe(5);
expect($("<div>").append($trs.eq(1).clone()).html().toLowerCase().replace(/\s*</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">5</td></tr>');
});
it("will render a new row by calling insertRow directly with a new model", function () {
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: new Backbone.Collection()
});
body.render();
body.insertRow({
id: 4
});
var $trs = body.$el.children();
expect($trs.length).toBe(1);
expect($("<div>").append($trs.eq(0).clone()).html().toLowerCase().replace(/\s*</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">4</td></tr>');
body.insertRow({
id: 5
}, {at: 0});
$trs = body.$el.children();
expect($trs.length).toBe(2);
expect($("<div>").append($trs.eq(0).clone()).html().toLowerCase().replace(/\s*</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">5</td></tr>');
});
it("will remove a row from the DOM if a model is removed from its collection", function () {
var m1 = body.collection.at(1);
body.collection.remove(m1);
var $trs = body.$el.children();
expect($trs.length).toBe(2);
expect($(body.el).html().toLowerCase().replace(/\s+</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">2</td></tr>' +
'<tr><td class="integer-cell editable sortable renderable id">3</td></tr>');
});
it("will remove a row from the DOM is removeRow is called directly with a model", function () {
var m1 = body.collection.at(1);
body.removeRow(m1);
var $trs = body.$el.children();
expect($trs.length).toBe(2);
expect($(body.el).html().toLowerCase().replace(/\s+</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">2</td></tr>' +
'<tr><td class="integer-cell editable sortable renderable id">3</td></tr>');
});
it("will refresh if its collection is reset", function () {
var eventFired = false;
var handler = function () {
eventFired = true;
};
body.collection.on("backgrid:refresh", handler);
body.collection.reset([{
id: 6
}]);
body.collection.off("backgrid:refresh", handler);
expect(eventFired).toBe(true);
var $trs = body.$el.children();
expect($trs.length).toBe(1);
expect($(body.el).html().toLowerCase().replace(/\s+</g, '<'))
.toBe('<tr><td class="integer-cell editable sortable renderable id">6</td></tr>');
});
it("will render rows using the Row class supplied in the constructor options", function () {
var CustomRow = Backgrid.Row.extend({});
spyOn(CustomRow.prototype, "render").and.callThrough();
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: col,
row: CustomRow
});
body.render();
expect(CustomRow.prototype.render).toHaveBeenCalled();
});
describe("maintain page size at page boundary", function () {
var col;
beforeEach(function () {
col = new Backbone.PageableCollection([
{id: 1},
{id: 2},
{id: 3}
], {
state: {
pageSize: 2
},
mode: "client"
});
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
});
it("when adding to a full page", function () {
col.add(new Backbone.Model({id: 4}));
expect(body.$el.find("tr").length).toBe(2);
});
it("when removing from a full page", function () {
col.remove(col.get(1));
expect(body.$el.find("tr").length).toBe(2);
});
});
it("will not display the empty row if collection is not empty", function () {
body = new Backgrid.Body({
emptyText: " ",
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty").length).toBe(0);
});
it("will not display the empty row if `options.emptyText` is not supplied", function () {
expect(body.$el.find("tr.empty").length).toBe(0);
col.reset();
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty").length).toBe(0);
});
it("will display the empty row if the collection is empty and `options.emptyText` is supplied", function () {
col.reset();
body = new Backgrid.Body({
emptyText: " ",
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty").length).toBe(1);
expect(body.$el.find("tr.empty > td").attr("colspan")).toBe("1");
});
it("will update the colspan of the empty row as columns are changed", function () {
col.reset();
body = new Backgrid.Body({
emptyText: " ",
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty > td").attr("colspan")).toBe("1");
body.columns.push({name: "age", cell: "integer"});
expect(body.$el.find("tr.empty > td").attr("colspan")).toBe("2");
});
it("will clear the empty row if a new model is added to an empty collection", function () {
col.reset();
body = new Backgrid.Body({
emptyText: " ",
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty").length).toBe(1);
col.add({id: 4});
expect(body.$el.find("tr.empty").length).toBe(0);
col.reset();
expect(body.$el.find("tr.empty").length).toBe(1);
body.insertRow({id: 5});
expect(body.$el.find("tr.empty").length).toBe(0);
});
it("will show the empty row if all rows are removed from the collection", function () {
col.reset({id: 4});
body = new Backgrid.Body({
emptyText: " ",
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.$el.find("tr.empty").length).toBe(0);
col.remove(col.at(0));
expect(body.$el.find("tr.empty").length).toBe(1);
body.insertRow({id: 5});
expect(body.$el.find("tr.empty").length).toBe(0);
body.removeRow(col.at(0));
expect(body.$el.find("tr.empty").length).toBe(1);
});
it("won't call render from updateEmptyRow if there is no emptyView", function () {
var pushColumn = function () {
body.columns.push({name: "age", cell: "integer"});
};
expect(pushColumn).not.toThrow();
});
it("#sort will throw a RangeError is direction is not ascending, descending or null", function () {
body = new Backgrid.Body({
collection: col,
columns: [{
name: "id",
cell: "integer"
}],
}).render();
expect(function () {
body.sort("id", "wat");
}).toThrow();
});
it("can sort the underlying collection using the default comparator", function () {
body = new Backgrid.Body({
collection: col,
columns: [{
name: "id",
cell: "integer"
}],
}).render();
body.collection.trigger("backgrid:sort", body.columns.at(0), "ascending");
expect(body.collection.toJSON()).toEqual([{id: 1}, {id: 2}, {id: 3}]);
expect(body.columns.at(0).get("direction"), "ascending");
body.collection.trigger("backgrid:sort", body.columns.at(0), "descending");
expect(body.collection.toJSON()).toEqual([{id: 3}, {id: 2}, {id: 1}]);
expect(body.columns.at(0).get("direction"), "descending");
col.at(0).cid = "c100";
col.at(1).cid = "c1";
col.at(2).cid = "c10";
body.collection.trigger("backgrid:sort", body.columns.at(0), null);
expect(body.collection.toJSON()).toEqual([{id: 2}, {id: 1}, {id: 3}]);
expect(body.columns.at(0).get("direction"), null);
});
it("can sort the underlying collection using a custom value extractor on `backgrid:sort`", function () {
var sortValue = function (model, attr) {
return 3 - model.get(attr);
};
body = new Backgrid.Body({
collection: col,
columns: [{
name: "id",
cell: "integer",
sortValue: sortValue
}],
}).render();
body.collection.trigger("backgrid:sort", body.columns.at(0), "ascending");
expect(body.collection.toJSON()).toEqual([{id: 3}, {id: 2}, {id: 1}]);
expect(body.columns.at(0).get("direction"), "ascending");
});
it("can sort on a server-mode Backbone.PageableCollection", function () {
var oldAjax = $.ajax;
$.ajax = function (settings) {
settings.success([{"total_entries": 3}, [{id: 2}, {id: 1}]]);
};
var col = new (Backbone.PageableCollection.extend({
url: "test-headercell"
}))([{id: 1}, {id: 2}], {
state: {
pageSize: 3
}
});
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer"
}],
collection: col
});
body.render();
expect(body.collection.at(0).get("id")).toBe(1);
expect(body.collection.at(1).get("id")).toBe(2);
var onBackgridSortedCallArgs = [];
col.on("backgrid:sorted", function () {
onBackgridSortedCallArgs.push([].slice.apply(arguments));
});
body.collection.trigger("backgrid:sort", body.columns.at(0), "descending");
expect(body.collection.at(0).get("id")).toBe(2);
expect(body.collection.at(1).get("id")).toBe(1);
expect(body.columns.at(0).get("direction"), "descending");
expect(onBackgridSortedCallArgs.length).toBe(1);
expect(onBackgridSortedCallArgs[0][0]).toBe(body.columns.at(0));
expect(onBackgridSortedCallArgs[0][1]).toBe("descending");
$.ajax = oldAjax;
});
it("can sort on a client-mode Backbone.PageableCollection", function () {
var col = new Backbone.PageableCollection([{id: 2}, {id: 1}, {id: 3}], {
state: {
pageSize: 1
},
mode: "client"
});
body = new Backgrid.Body({
columns: [{
name: "id",
cell: "integer",
sortValue: function (model, attr) {
return 3 - model.get(attr);
}
}],
collection: col
});
body.render();
var onBackgridSortedCallArgs = [];
col.on("backgrid:sorted", function () {
onBackgridSortedCallArgs.push([].slice.apply(arguments));
});
col.trigger("backgrid:sort", body.columns.at(0), "ascending");
expect(body.collection.toJSON()).toEqual([{id: 3}]);
expect(body.columns.at(0).get("direction"), "ascending");
expect(onBackgridSortedCallArgs.length).toBe(1);
expect(onBackgridSortedCallArgs[0][0]).toBe(body.columns.at(0));
expect(onBackgridSortedCallArgs[0][1]).toBe("ascending");
expect(onBackgridSortedCallArgs[0][2]).toBe(col);
body.collection.getPage(2);
expect(body.collection.toJSON()).toEqual([{id: 2}]);
body.collection.getPage(3);
expect(body.collection.toJSON()).toEqual([{id: 1}]);
body.collection.getFirstPage();
col.trigger("backgrid:sort", body.columns.at(0), "descending");
expect(body.columns.at(0).get("direction"), "descending");
expect(body.collection.toJSON()).toEqual([{id: 1}]);
expect(onBackgridSortedCallArgs.length).toBe(2);
expect(onBackgridSortedCallArgs[1][0]).toBe(body.columns.at(0));
expect(onBackgridSortedCallArgs[1][1]).toBe("descending");
expect(onBackgridSortedCallArgs[1][2]).toBe(col);
col.trigger("backgrid:sort", body.columns.at(0), null);
expect(body.columns.at(0).get("direction"), null);
expect(body.collection.toJSON()).toEqual([{id: 2}]);
expect(onBackgridSortedCallArgs.length).toBe(3);
expect(onBackgridSortedCallArgs[2][0]).toBe(body.columns.at(0));
expect(onBackgridSortedCallArgs[2][1]).toBe(null);
expect(onBackgridSortedCallArgs[2][2]).toBe(col);
});
it("will put the next editable and renderable cell in edit mode when a save or one of the navigation commands is triggered via backgrid:edited from the collection", function () {
var people = new Backbone.Collection([
{name: "alice", age: 28, married: false},
{name: "bob", age: 30, married: true}
]);
var columns = new Backgrid.Columns([{
name: "name",
cell: "string"
}, {
name: "age",
cell: "integer",
editable: false
}, {
name: "sex",
cell: "boolean",
renderable: false
}]);
var body = new Backgrid.Body({
collection: people,
columns: columns
});
body.render();
body.rows[0].cells[0].enterEditMode();
// Just making sure a cell has exited edit mode before the next cell goes
// into edit mode. Fixes #187.
var oldExitEditMode = body.rows[0].cells[0].exitEditMode;
var callOrders = [];
body.rows[0].cells[0].exitEditMode = function () {
callOrders.push("exit");
return oldExitEditMode.apply(this, arguments);
};
var oldEnterEditMode = body.rows[1].cells[0].enterEditMode;
body.rows[1].cells[0].enterEditMode = function () {
callOrders.push("enter");
return oldEnterEditMode.apply(this, arguments);
};
// right
people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 9}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(true);
expect(callOrders[0]).toBe("exit");
expect(callOrders[1]).toBe("enter");
body.rows[0].cells[0].exitEditMode = oldExitEditMode;
body.rows[1].cells[0].enterEditMode = oldEnterEditMode;
// left
people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 9, shiftKey: true}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(true);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false);
// down
people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 40}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(true);
// up
people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 38}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(true);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false);
// enter
people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 13}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false);
// esc
body.rows[1].cells[0].enterEditMode();
people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 27}));
expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false);
expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false);
});
it("will not throw an exception when backgrid:edited is fired on a shared model", function () {
var people = new Backbone.Collection([
{name: "alice", age: 28, married: false},
{name: "bob", age: 30, married: true}
]);
var columns = new Backgrid.Columns([{
name: "name",
cell: "string"
}, {
name: "age",
cell: "integer",
editable: false
}, {
name: "sex",
cell: "boolean",
renderable: false
}]);
var body = new Backgrid.Body({
collection: people,
columns: columns
});
body.render();
var columns2 = new Backgrid.Columns([{
name: "name",
cell: "string"
}]);
var body2 = new Backgrid.Body({
collection: people,
columns: columns2
});
body2.render();
body.rows[0].cells[0].enterEditMode();
var testTrigger = function() {
people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 9}));
};
expect(testTrigger).not.toThrow();
});
});