Merge "Angular Table Directive"

This commit is contained in:
Jenkins 2016-05-03 12:32:04 +00:00 committed by Gerrit Code Review
commit d965f730c5
6 changed files with 632 additions and 0 deletions

View File

@ -0,0 +1,110 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
(function() {
'use strict';
angular
.module('horizon.framework.widgets.table')
.directive('hzCell', hzCell);
hzCell.$inject = ['$compile', '$filter'];
/**
* @ngdoc directive
* @name horizon.framework.widgets.table.directive:hzCell
* @description
* The `hzCell` directive allows you to customize your cell content.
* When specifying your table configuration object, you may pass in a
* template per each column.
*
* You should define a template when you want to format data or show more
* complex content (e.g conditionally show different icons or a link).
* You should reference the cell's 'item' attribute in the template if
* you need access to the cell's data. The attributes 'column' and 'item'
* should be defined outside of this directive. See example below.
*
* It should ideally be used within the context of the `hz-dynamic-table` directive.
* The params passed into `hz-dynamic-table` can be used in the custom template,
* including the 'table' scope.
*
* @restrict E
*
* @scope
* @example
*
* var config = {
* selectAll: true,
* expand: true,
* columns: [
* {id: 'a', title: 'Header A', priority: 1},
* {id: 'b', title: 'Header B', priority: 2},
* {id: 'c', title: 'Header C', priority: 1, sortDefault: true},
* {id: 'd', title: 'Header D', priority: 2,
* template: '<span class="fa fa-bolt">{$ item.id $}</span>',
* filters: [someFilterFunction, 'uppercase']}
* ]
* };
*
* ```
* <tbody>
* <tr ng-repeat="item in items track by $index">
* <td ng-repeat="column in config.columns"
* class="{$ column.classes $}">
* <hz-cell></hz-cell>
* </td>
* </tr>
* </tbody>
* ```
*
*/
function hzCell($compile, $filter) {
var directive = {
restrict: 'E',
scope: false,
link: link
};
return directive;
///////////////////
function link(scope, element) {
var column = scope.column;
var item = scope.item;
var html;
// if template provided, render, and place into cell
if (column && column.template) {
html = $compile(column.template)(scope);
} else {
// apply filters to cell data if applicable
html = item[column.id];
if (column && column.filters) {
for (var i = 0; i < column.filters.length; i++) {
var filter = column.filters[i];
// call horizon framework filter function if provided
if (angular.isFunction(filter)) {
html = filter(item[column.id]);
// call angular filters
} else {
html = $filter(filter)(item[column.id]);
}
}
}
}
element.append(html);
}
}
})();

View File

@ -0,0 +1,97 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
(function() {
'use strict';
angular
.module('horizon.framework.widgets.table')
.directive('hzDetailRow', hzDetailRow);
hzDetailRow.$inject = ['horizon.framework.widgets.basePath',
'$http',
'$compile',
'$parse',
'$templateCache'];
/**
* @ngdoc directive
* @name horizon.framework.widgets.table.directive:hzDetailRow
* @description
* The `hzDetailRow` directive is the detail drawer per each row triggered by
* the hzExpandDetail. Use this option for customization and complete control over what
* is rendered. If a custom template is not provided, it will use the template
* found at hz-detail-row.html. 'config.columns' and 'item' must be provided for the
* default template to work. See example below.
*
* It should ideally be used within the context of the `hz-dynamic-table` directive.
* The params passed into `hz-dynamic-table` can be used in the custom template,
* including the 'table' scope.
*
* @restrict E
*
* @param {object=} templateUrl path to custom html template you want to
* place inside the detail drawer (optional)
*
* @scope
* @example
*
* ```
* <tbody>
* <tr ng-repeat-start="item in items track by $index">
* <td ng-show="config.expand" class="expander">
* <span class="fa fa-chevron-right"
* hz-expand-detail
* duration="200">
* </span>
* </td>
* <td ng-repeat="column in config.columns" class="{$ column.classes $}">
* item[column.id]
* </td>
* </tr>
* <tr ng-if="config.expand" ng-repeat-end class="detail-row">
* <td class="detail" colspan="100">
* <hz-detail-row template-url="config.detailsTemplate">
* </hz-detail-row>
* </td>
* </tr>
* </tbody>
*
* ```
*
*/
function hzDetailRow(basePath, $http, $compile, $parse, $templateCache) {
var directive = {
restrict: 'E',
scope: false,
link: link
};
return directive;
function link(scope, element, attrs) {
var templateUrl = $parse(attrs.templateUrl)(scope);
if (angular.isUndefined(templateUrl)) {
templateUrl = basePath + 'table/hz-detail-row.html';
}
$http.get(templateUrl, { cache: $templateCache })
.then(function(response) {
var template = response.data;
element.append($compile(template)(scope));
});
}
}
})();

View File

@ -0,0 +1,18 @@
<!--
Default detail row template
May be overridden.
The responsive columns that disappear typically should reappear here
with the same responsive priority that they disappear.
E.g. table header with rsp-p2 should be here with rsp-alt-p2
The layout should minimize vertical space to reduce scrolling.
-->
<div class="row">
<span class="rsp-alt-p2">
<dl class="col-sm-2" ng-repeat="column in config.columns">
<dt translate>{$ column.title $}</dt>
<dd translate><hz-cell></hz-cell></dd>
</dl>
</span>
</div>

View File

@ -0,0 +1,104 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
(function() {
'use strict';
angular
.module('horizon.framework.widgets.table')
.directive('hzDynamicTable', hzDynamicTable);
hzDynamicTable.$inject = ['horizon.framework.widgets.basePath'];
/**
* @ngdoc directive
* @name horizon.framework.widgets.table.directive:hzDynamicTable
* @restrict E
*
* @param {object} config column definition used to generate the table (required)
* @param {object} items displayed row collection (required)
* @param {object} safeItems safe row collection (required)
* @param {object=} table any additional information that are
* passed down to child widgets (e.g hz-cell) (optional)
* @param {object=} batchActions batch actions for the table (optional)
* @param {object=} itemActions item actions for each item/row (optional)
* @param {object=} filterFacets Facets allowed for searching, if not provided,
* default to simple text search (optional)
* @param {function=} resultHandler function that is called with return value
* from a clicked actions perform function passed into `actions` directive (optional)
*
* @description
* The `hzDynamicTable` directive generates all the HTML content for a table.
* You will need to pass in three attributes: `config`, `items`, and `safe-src-items`.
*
* This directive is built off the Smart-table module, so `items` and `safe-src-items`
* are passed into `st-table` and `st-safe-src` attribute, respectively.
* Note: `st-safe-src' is used for async data, to keep track of modifications to the
* collection. Also, 'name' is the key used to retrieve cell data from base
* 'displayedCollection'
*
* @example
*
* var config = {
* selectAll: true,
* expand: true,
* trackId: 'id',
* columns: [
* {id: 'a', title: 'A', priority: 1},
* {id: 'b', title: 'B', priority: 2},
* {id: 'c', title: 'C', priority: 1, sortDefault: true},
* {id: 'd', title: 'D', priority: 2, filters: [myFilter, 'yesno']}
* ]
* };
* ```
* <hz-dynamic-table
* config='config'
* items='items'
* safe-src-items="safeSrcItems">
* </hz-dynamic-table>
*
* <hz-dynamic-table
* config='config'
* items='items'
* safe-src-items="safeSrcItems"
* table="table"
* batchActions="batchActions"
* itemActions="itemActions"
* filterFacets="filterFacets"
* resultHandler="resultHandler">
* </hz-dynamic-table>
* ```
*
*/
function hzDynamicTable(basePath) {
var directive = {
restrict: 'E',
scope: {
config: '=',
items: '=',
safeSrcItems: '=',
table: '=',
batchActions: '=?',
itemActions: '=?',
filterFacets: '=?',
resultHandler: '=?'
},
templateUrl: basePath + 'table/hz-dynamic-table.html'
};
return directive;
}
})();

View File

@ -0,0 +1,101 @@
<!--
Dynamic table template
-->
<hz-magic-search-context filter-facets="filterFacets">
<hz-magic-search-bar ng-if="filterFacets">
</hz-magic-search-bar>
<actions ng-if="filterFacets && batchActions" allowed="batchActions" type="batch" result-handler="resultHandler"></actions>
<table
hz-table ng-cloak
st-magic-search
st-table="items"
st-safe-src="safeSrcItems"
default-sort="name"
default-sort-reverse="false"
class="table table-striped table-rsp table-detail">
<thead>
<!--
Table-batch-actions:
Batch actions - searching, creating, and deleting.
-->
<tr>
<th colspan="100" class="search-header">
<hz-search-bar ng-if="!filterFacets" group-classes="input-group" icon-classes="fa-search">
<actions ng-if="batchActions" allowed="batchActions" type="batch" result-handler="resultHandler"></actions>
</hz-search-bar>
</th>
</tr>
<!--
Table-column-headers:
Set selectAll to True if you want to enable select all checkbox.
Set expand to True if you want to inline details.
-->
<tr>
<th ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox" hz-select-all="items">
</th>
<th ng-show="config.expand" class="expander"></th>
<th ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}"
st-sort="{$ column.id $}"
ng-attr-st-sort-default="{$ column.sortDefault $}"
translate>
{$ column.title $}
</th>
</tr>
</thead>
<tbody>
<!--
Table-rows:
classes rsp-p1 rsp-p2 are responsive priority as user resizes window.
-->
<tr ng-repeat-start="item in items track by item[config.trackId]"
ng-class="{'st-selected': checked[item[config.trackId]]}">
<td ng-show="config.selectAll" class="multi_select_column">
<input type="checkbox"
ng-model="tCtrl.selections[item[config.trackId]].checked"
hz-select="item">
</td>
<td ng-show="config.expand" class="expander">
<span class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</span>
</td>
<td ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}">
<hz-cell></hz-cell>
</td>
<td class="action_column">
<!--
Table-row-action-column:
Actions taken here apply to a single item/row.
-->
<actions ng-if="itemActions" allowed="itemActions" type="row" item="item" result-handler="resultHandler"></actions>
</td>
</tr>
<!--
Detail-row:
Contains detailed information on this item.
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<tr ng-if="config.expand" ng-repeat-end class="detail-row">
<td class="detail" colspan="100">
<hz-detail-row template-url="config.detailsTemplateUrl">
</hz-detail-row>
</td>
</tr>
<tr hz-no-items items="items"></tr>
</tbody>
<!--
Table-footer:
This is where we display number of items and pagination controls.
-->
<tfoot hz-table-footer items="items"></tfoot>
</table>
</hz-magic-search-context>

View File

@ -0,0 +1,202 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
function digestMarkup(scope, compile, markup) {
var element = angular.element(markup);
compile(element)(scope);
scope.$apply();
return element;
}
describe('hzDynamicTable directive', function () {
var $scope, $compile, markup;
beforeEach(module('templates'));
beforeEach(module('smart-table'));
beforeEach(module('horizon.framework'));
beforeEach(inject(function ($injector) {
$compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
$scope.config = {
selectAll: true,
expand: true,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1},
{id: 'type', title: 'Type', priority: 2},
{id: 'diet', title: 'Diet', priority: 1, sortDefault: true}
]
};
$scope.safeTableData = [
{ id: '1', animal: 'cat', type: 'mammal', diet: 'fish', domestic: true },
{ id: '2', animal: 'snake', type: 'reptile', diet: 'mice', domestic: false },
{ id: '3', animal: 'sparrow', type: 'bird', diet: 'worms', domestic: false }
];
$scope.fakeTableData = [];
markup =
'<hz-dynamic-table config="config" items="fakeTableData" safe-src-items="safeTableData">' +
'</hz-dynamic-table>';
}));
it('has the correct number of column headers', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element).toBeDefined();
expect($element.find('thead tr:eq(1) th').length).toBe(5);
});
it('displays selectAll checkbox when config selectAll set to True', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('thead tr:eq(1) th:first input').attr(
'hz-select-all')).toBe('items');
});
it('does not display selectAll checkbox when config selectAll set to False', function() {
$scope.config = {
selectAll: false,
expand: true,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1},
{id: 'type', title: 'Type', priority: 2},
{id: 'diet', title: 'Diet', priority: 1, sortDefault: true}
]
};
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('thead tr:eq(1) th:first').hasClass('ng-hide')).toBe(true);
});
it('displays expander when config expand set to True', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('thead tr:eq(1) th:eq(1)').hasClass('expander')).toBe(true);
});
it('does not display expander when config expand set to False', function() {
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1},
{id: 'type', title: 'Type', priority: 2},
{id: 'diet', title: 'Diet', priority: 1, sortDefault: true}
]
};
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('thead tr:eq(1) th:eq(1)').hasClass('ng-hide')).toBe(true);
});
it('has the correct responsive priority classes', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr').length).toBe(7);
expect($element.find('tbody tr:eq(0) td').length).toBe(6);
expect($element.find('tbody tr:eq(2) td:eq(2)').hasClass('rsp-p1')).toBe(true);
expect($element.find('tbody tr:eq(2) td:eq(3)').hasClass('rsp-p2')).toBe(true);
expect($element.find('tbody tr:eq(2) td:eq(4)').hasClass('rsp-p1')).toBe(true);
});
it('has the correct number of rows (including detail rows and no items row)', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr').length).toBe(7);
expect($element.find('tbody tr:eq(0) td').length).toBe(6);
expect($element.find('tbody tr:eq(2) td:eq(2)').text()).toContain('snake');
expect($element.find('tbody tr:eq(2) td:eq(3)').text()).toContain('reptile');
expect($element.find('tbody tr:eq(2) td:eq(4)').text()).toContain('mice');
});
describe('hzDetailRow directive', function() {
it('compiles default detail row template', function() {
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr:eq(3) dt').text()).toContain('Animal');
expect($element.find('tbody tr:eq(3) dd').text()).toContain('snake');
expect($element.find('tbody tr:eq(3) dt').text()).toContain('Type');
expect($element.find('tbody tr:eq(3) dd').text()).toContain('reptile');
expect($element.find('tbody tr:eq(3) dt').text()).toContain('Diet');
expect($element.find('tbody tr:eq(3) dd').text()).toContain('mice');
});
});
describe('hzCell directive', function() {
it('compiles template passed in from column configuration', function() {
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', classes: 'rsp-p1'},
{id: 'type', title: 'Type', classes: 'rsp-p2',
template: '<span class="fa fa-bolt">{$ item.type $}</span>'},
{id: 'diet', title: 'Diet', classes: 'rsp-p1', sortDefault: true}
]
};
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr:eq(2) td:eq(3) span').hasClass('fa fa-bolt')).toBe(true);
expect($element.find('tbody tr:eq(2) td:eq(3) span').text()).toBe('bird');
});
it('properly filters the cell content given the filter name', function() {
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1},
{id: 'type', title: 'Type', priority: 2,
template: '<span class="fa fa-bolt">{$ item.type $}</span>'},
{id: 'diet', title: 'Diet', priority: 1, sortDefault: true},
{id: 'domestic', title: 'Domestic', priority: 2, filters: ['yesno']}
]
};
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr:eq(0) td:eq(5)').text()).toContain('Yes');
expect($element.find('tbody tr:eq(1) td:eq(5)').text()).toContain('No');
expect($element.find('tbody tr:eq(2) td:eq(5)').text()).toContain('No');
});
it('properly filters the cell content given a filter function', function() {
function ishFunc(input) {
return input.concat('-ish');
}
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1},
{id: 'type', title: 'Type', priority: 2, filters: [ishFunc]},
{id: 'diet', title: 'Diet', priority: 1, sortDefault: true},
{id: 'domestic', title: 'Domestic', priority: 2}
]
};
var $element = digestMarkup($scope, $compile, markup);
expect($element.find('tbody tr:eq(0) td:eq(3)').text()).toContain('mammal-ish');
expect($element.find('tbody tr:eq(1) td:eq(3)').text()).toContain('reptile-ish');
expect($element.find('tbody tr:eq(2) td:eq(3)').text()).toContain('bird-ish');
});
});
});
}());