diff --git a/djblets/settings.py b/djblets/settings.py
index e75cec779960f7d7d7e03cfd2378675e282af4d0..2233c219381ce75e39b8e846fe9caa0d2b308962 100644
--- a/djblets/settings.py
+++ b/djblets/settings.py
@@ -84,6 +84,8 @@ PIPELINE_JS = {
             'djblets/js/configForms/models/tests/listItemModelTests.js',
             'djblets/js/configForms/views/tests/listItemViewTests.js',
             'djblets/js/configForms/views/tests/listViewTests.js',
+            'djblets/js/configForms/views/tests/tableItemViewTests.js',
+            'djblets/js/configForms/views/tests/tableViewTests.js',
         ),
         'output_filename': 'djblets/js/tests.min.js',
     },
diff --git a/djblets/static/djblets/js/configForms/views/listItemView.js b/djblets/static/djblets/js/configForms/views/listItemView.js
index 1a0a7087064ee58a65726c17e07ab643d6b839c8..49220263f3123ba8a9eca9ccaf501db66e72dab0 100644
--- a/djblets/static/djblets/js/configForms/views/listItemView.js
+++ b/djblets/static/djblets/js/configForms/views/listItemView.js
@@ -45,7 +45,7 @@ Djblets.Config.ListItemView = Backbone.View.extend({
         this.$el
             .empty()
             .append(this.template(this.model.attributes));
-        this.addActions(this.$el);
+        this.addActions(this.getActionsParent());
 
         return this;
     },
@@ -61,6 +61,16 @@ Djblets.Config.ListItemView = Backbone.View.extend({
     },
 
     /*
+     * Returns the container for the actions.
+     *
+     * This defaults to being this element, but it can be overridden to
+     * return a more specific element.
+     */
+    getActionsParent: function() {
+        return this.$el;
+    },
+
+    /*
      * Displays a spinner on the item.
      *
      * This can be used to show that the item is being loaded from the
diff --git a/djblets/static/djblets/js/configForms/views/listView.js b/djblets/static/djblets/js/configForms/views/listView.js
index 3e6149ed248b9e4a979f87cbcbb4950094ec81ce..98c1cad28a9f16b2c60c239ac85cf4f28c8a310c 100644
--- a/djblets/static/djblets/js/configForms/views/listView.js
+++ b/djblets/static/djblets/js/configForms/views/listView.js
@@ -12,6 +12,7 @@
 Djblets.Config.ListView = Backbone.View.extend({
     tagName: 'ul',
     className: 'config-forms-list',
+    defaultItemView: Djblets.Config.ListItemView,
 
     /*
      * Initializes the view.
@@ -19,7 +20,7 @@ Djblets.Config.ListView = Backbone.View.extend({
     initialize: function(options) {
         var collection = this.model.collection;
 
-        this.ItemView = options.ItemView || Djblets.Config.ListItemView;
+        this.ItemView = options.ItemView || this.defaultItemView,
 
         this.once('rendered', function() {
             this.listenTo(collection, 'add', this._addItem);
diff --git a/djblets/static/djblets/js/configForms/views/tableItemView.js b/djblets/static/djblets/js/configForms/views/tableItemView.js
index d9a9ac73bfc1b47549c1da39421acf14574e39aa..8894f5a2a89687090c5a3e3d6d68b3faa1d3b8ff 100644
--- a/djblets/static/djblets/js/configForms/views/tableItemView.js
+++ b/djblets/static/djblets/js/configForms/views/tableItemView.js
@@ -9,11 +9,21 @@ Djblets.Config.TableItemView = Djblets.Config.ListItemView.extend({
 
     template: _.template([
         '<td>',
-        ' <% if (editURL) { %>',
-        '  <a href="<%- editURL %>"><%- text %></a>',
-        ' <% } else { %>',
-        '  <%- text %>',
-        ' <% } %>',
+        '<% if (editURL) { %>',
+        '<a href="<%- editURL %>"><%- text %></a>',
+        '<% } else { %>',
+        '<%- text %>',
+        '<% } %>',
         '</td>'
-    ].join(''))
+    ].join('')),
+
+    /*
+     * Returns the container for the actions.
+     *
+     * This defaults to being the last cell in the row, but this can be
+     * overridden to provide a specific cell or an element within.
+     */
+    getActionsParent: function() {
+        return this.$('td:last');
+    }
 });
diff --git a/djblets/static/djblets/js/configForms/views/tableView.js b/djblets/static/djblets/js/configForms/views/tableView.js
index dce29f013e7a1be2de90a5475f2e28e64b7a357c..a352a4c817b82e182140ab0d69e52402f6ecb20d 100644
--- a/djblets/static/djblets/js/configForms/views/tableView.js
+++ b/djblets/static/djblets/js/configForms/views/tableView.js
@@ -6,7 +6,27 @@
  */
 Djblets.Config.TableView = Djblets.Config.ListView.extend({
     tagName: 'table',
+    defaultItemView: Djblets.Config.TableItemView,
 
+    /*
+     * Renders the view.
+     *
+     * If the element does not already have a <tbody>, one will be added.
+     * All items will go under this.
+     */
+    render: function() {
+        var $body = this.getBody();
+
+        if ($body.length === 0) {
+            this.$el.append('<tbody/>');
+        }
+
+        return _super(this).render.call(this);
+    },
+
+    /*
+     * Returns the body element where items will be added.
+     */
     getBody: function() {
         return this.$('tbody');
     }
diff --git a/djblets/static/djblets/js/configForms/views/tests/listItemViewTests.js b/djblets/static/djblets/js/configForms/views/tests/listItemViewTests.js
index 33492fbe9c7ffe661919e31a278dada5f324031b..8e0300b65d50e5ca73b27979d43adc9f63fab42b 100644
--- a/djblets/static/djblets/js/configForms/views/tests/listItemViewTests.js
+++ b/djblets/static/djblets/js/configForms/views/tests/listItemViewTests.js
@@ -189,7 +189,9 @@ suite('djblets/configForms/views/ListItemView', function() {
                 }),
                 $button;
 
-            itemView.actionHandlers.mybutton = '_onMyButtonClick';
+            itemView.actionHandlers = {
+                mybutton: '_onMyButtonClick'
+            };
             itemView._onMyButtonClick = function() {};
             spyOn(itemView, '_onMyButtonClick');
 
@@ -220,7 +222,9 @@ suite('djblets/configForms/views/ListItemView', function() {
                 }),
                 $checkbox;
 
-            itemView.actionHandlers.mybutton = '_onMyButtonClick';
+            itemView.actionHandlers = {
+                mybutton: '_onMyButtonClick'
+            };
             itemView._onMyButtonClick = function() {};
             spyOn(itemView, '_onMyButtonClick');
 
diff --git a/djblets/static/djblets/js/configForms/views/tests/tableItemViewTests.js b/djblets/static/djblets/js/configForms/views/tests/tableItemViewTests.js
new file mode 100644
index 0000000000000000000000000000000000000000..832ed0c0612dd5a3721b5339e35fe1fd6b2bb4ec
--- /dev/null
+++ b/djblets/static/djblets/js/configForms/views/tests/tableItemViewTests.js
@@ -0,0 +1,90 @@
+suite('djblets/configForms/views/TableItemView', function() {
+    describe('Rendering', function() {
+        describe('Item display', function() {
+            it('With editURL', function() {
+                var item = new Djblets.Config.ListItem({
+                        editURL: 'http://example.com/',
+                        text: 'Label'
+                    }),
+                    itemView = new Djblets.Config.TableItemView({
+                        model: item
+                    });
+
+                itemView.render();
+                expect(itemView.$el.html().strip()).toBe(
+                    '<td>' +
+                    '<span class="config-forms-list-item-actions"></span>' +
+                    '<a href="http://example.com/">Label</a>' +
+                    '</td>');
+            });
+
+            it('Without editURL', function() {
+                var item = new Djblets.Config.ListItem({
+                        text: 'Label'
+                    }),
+                    itemView = new Djblets.Config.TableItemView({
+                        model: item
+                    });
+
+                itemView.render();
+                expect(itemView.$el.html().strip()).toBe(
+                    '<td>' +
+                    '<span class="config-forms-list-item-actions"></span>' +
+                    'Label' +
+                    '</td>');
+            });
+        });
+
+        describe('Action placement', function() {
+            it('Default template', function() {
+                var item = new Djblets.Config.ListItem({
+                        text: 'Label',
+                        actions: [
+                            {
+                                id: 'mybutton',
+                                label: 'Button'
+                            }
+                        ]
+                    }),
+                    itemView = new Djblets.Config.TableItemView({
+                        model: item
+                    }),
+                    $button;
+
+                itemView.render();
+
+                $button = itemView.$('td:last .btn');
+                expect($button.length).toBe(1);
+                expect($button.text()).toBe('Button');
+            });
+
+            it('Custom template', function() {
+                var CustomTableItemView = Djblets.Config.TableItemView.extend({
+                        template: _.template([
+                            '<td></td>',
+                            '<td></td>'
+                        ].join(''))
+                    }),
+                    item = new Djblets.Config.ListItem({
+                        text: 'Label',
+                        actions: [
+                            {
+                                id: 'mybutton',
+                                label: 'Button'
+                            }
+                        ]
+                    }),
+                    itemView = new CustomTableItemView({
+                        model: item
+                    }),
+                    $button;
+
+                itemView.render();
+
+                $button = itemView.$('td:last .btn');
+                expect($button.length).toBe(1);
+                expect($button.text()).toBe('Button');
+            });
+        });
+    });
+});
diff --git a/djblets/static/djblets/js/configForms/views/tests/tableViewTests.js b/djblets/static/djblets/js/configForms/views/tests/tableViewTests.js
new file mode 100644
index 0000000000000000000000000000000000000000..b18637bbaa15ee915a6ea60e75c1919e3efd8324
--- /dev/null
+++ b/djblets/static/djblets/js/configForms/views/tests/tableViewTests.js
@@ -0,0 +1,73 @@
+suite('djblets/configForms/views/TableView', function() {
+    describe('Manages rows', function() {
+        var collection,
+            list,
+            tableView;
+
+        beforeEach(function() {
+            collection = new Backbone.Collection(
+                [
+                    {text: 'Item 1'},
+                    {text: 'Item 2'},
+                    {text: 'Item 3'}
+                ], {
+                    model: Djblets.Config.ListItem
+                });
+
+            list = new Djblets.Config.List({}, {
+                collection: collection
+            });
+
+            tableView = new Djblets.Config.TableView({
+                model: list
+            });
+            tableView.render();
+        });
+
+        it('On render', function() {
+            var $rows;
+
+            $rows = tableView.$('tr');
+            expect($rows.length).toBe(3);
+            expect($rows.eq(0).text().strip()).toBe('Item 1');
+            expect($rows.eq(1).text().strip()).toBe('Item 2');
+            expect($rows.eq(2).text().strip()).toBe('Item 3');
+        });
+
+        it('On add', function() {
+            var $rows;
+
+            collection.add({
+                text: 'Item 4'
+            });
+
+            $rows = tableView.$('tr');
+            expect($rows.length).toBe(4);
+            expect($rows.eq(3).text().strip()).toBe('Item 4');
+        });
+
+        it('On remove', function() {
+            var $rows;
+
+            collection.remove(collection.at(0));
+
+            $rows = tableView.$('tr');
+            expect($rows.length).toBe(2);
+            expect($rows.eq(0).text().strip()).toBe('Item 2');
+        });
+
+        it('On reset', function() {
+            var $rows;
+
+            collection.reset([
+                {text: 'Foo'},
+                {text: 'Bar'}
+            ]);
+
+            $rows = tableView.$('tr');
+            expect($rows.length).toBe(2);
+            expect($rows.eq(0).text().strip()).toBe('Foo');
+            expect($rows.eq(1).text().strip()).toBe('Bar');
+        });
+    });
+});
