Modify hz-cell to use hz-field

hz-cell doesn't use params, which causes lots of problems when trying to
render changes to data...since it evaluates only once, reading what's assumed
to be on scope.

This patch adds 'table,' 'column,' and 'item' params so they may be better
watched by the directive.

hz-field does use params and thus is useful in many situations outside
of hz-cell.  This patch provides hz-field and lets hz-cell use it so
all output logic is shared.

hz-field also: a) accepts 'values' as a column configuration as we discussed
in the last mid-cycle, so codes, etc. may be supplied without use of a filter;
and b) accepts 'urlFunction' which translates the given item to a url which is
linked on the outputted name (rather than having to write a template for a
common link function).

Follow-on patches will demonstrate use.

Change-Id: I0835a90e61d0e708233da795964595f88616388c
Partially-Implements: blueprint angular-registry
This commit is contained in:
Matt Borland 2016-06-24 12:51:31 -06:00
parent 4242d2c29d
commit 146256f8c7
8 changed files with 244 additions and 32 deletions

View File

@ -0,0 +1,131 @@
/**
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* 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.property')
.directive('hzField', hzField);
hzField.$inject = ['$filter'];
/**
* @ngdoc directive
* @name hzField
* @param config {Object} - The field definition object, described below
* @param item {Object} - The object containing the property from config.id
* @description
* The `hzField` directive allows you to output an object's property using
* formatting as provided by a field configuration.
*
* The config object describes a single field, and the config object's 'id'
* property matches the name of a property in the 'item' parameter. For
* example, if config.id is 'name' then there should be an item.name that
* is evaluated for display using the logic described below.
*
* The field configuration may transform the data in the item's property
* using either a set of single-argument filters or functions, specified by
* the 'filters' property, or using the 'values' object in which the item
* property is mapped via the keys to the values in the given object. Note
* that a combination of 'filters' and 'values' may be used; in this case
* the filters are evaluated first. This allows for translations that will
* map to keys first (e.g. upper-casing a string with a filter so it matches
* upper-case keys), and allows the values provided in the 'values' mapping
* to be the final value produced. The 'urlFunction' option allows for a
* a function to be given, where the item is the sole parameter and the result
* should be a URL.
*
* @restrict E
*
* @scope
* @example
*
* var config = {id: 'a', title: 'Header A', priority: 1};
*
* // Using urlFunction to create a link
* var linked = {id: 'b', title: 'Header B', priority: 2, urlFunction: myUrlFunction},
*
* // Using defaultSort
* var defaultSort = {id: 'c', title: 'Header C', priority: 1, sortDefault: true};
*
* // Using filters (can be combined with 'values')
* var filtered = {id: 'd', title: 'Header D', priority: 2,
* filters: [someFilterFunction, 'uppercase']};
*
* // Using value mappings
* var mapped = {id: 'e', title: 'Header E', priority: 1,
* values: {
* 'a': 'apple',
* 'j': 'jacks'
* }
* };
*
* function myUrlFunction(item) {
* return '/my/path/' + item.id;
* }
*
* ```
* <hz-field config="config" item="item"></hz-field>
* ```
*
*/
function hzField($filter) {
var directive = {
restrict: 'E',
scope: {
config: "=",
item: "="
},
link: link
};
return directive;
///////////////////
function link(scope, element) {
var config = scope.config;
var item = scope.item;
var propValue = item[config.id];
var output = propValue;
if (config && config.filters) {
for (var i = 0; i < config.filters.length; i++) {
var filter = config.filters[i];
// call horizon framework filter function if provided
if (angular.isFunction(filter)) {
output = filter(propValue);
// call angular filters
} else {
output = $filter(filter)(propValue);
}
}
}
if (config && config.values) {
// apply mapping values to the data if applicable
output = config.values[output];
}
var url;
if (config && config.urlFunction) {
url = config.urlFunction(item);
}
if (url) {
element.append(angular.element('<a>').attr('href', url).append(output));
} else {
element.append(output);
}
}
}
})();

View File

@ -0,0 +1,28 @@
/*
* (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* 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';
/**
* @ngdoc overview
* @name horizon.framework.widgets.property
* @description
* This module provides support for displaying properties of registered resource
* types.
*/
angular.module('horizon.framework.widgets.property', []);
})();

View File

@ -20,26 +20,29 @@
.module('horizon.framework.widgets.table')
.directive('hzCell', hzCell);
hzCell.$inject = ['$compile', '$filter'];
hzCell.$inject = ['$compile'];
/**
* @ngdoc directive
* @name horizon.framework.widgets.table.directive:hzCell
* @param table {Object} - The table/controller context
* @param column {Object} - The column definition object, described below
* @param item {Object} - The object containing the property from column.id
* @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.
*
* See the documentation on hz-field for details on how to specify formatting
* based on the column configuration.
*
* 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.
* you need access to the cell's data. 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. 'table' can be referenced if you want to pass in an
* outside scope.
* 'table' can be referenced in a template if you want to pass in an outside scope.
*
* @restrict E
*
@ -55,27 +58,37 @@
* {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']}
* filters: [someFilterFunction, 'uppercase']},
* {id: 'e', title: 'Header E', priority: 1,
* values: {
* 'a': 'apple',
* 'j': 'jacks'
* }
* }
* ]
* };
*
* ```
* <tbody>
* <tbody ng-controller="TableCtrl as table">
* <tr ng-repeat="item in items track by $index">
* <td ng-repeat="column in config.columns"
* class="{$ column.classes $}">
* <hz-cell></hz-cell>
* <hz-cell table="table" column="column" item="item"></hz-cell>
* </td>
* </tr>
* </tbody>
* ```
*
*/
function hzCell($compile, $filter) {
function hzCell($compile) {
var directive = {
restrict: 'E',
scope: false,
scope: {
table: '=',
column: '=',
item: '='
},
link: link
};
return directive;
@ -84,26 +97,14 @@
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) {
// if template provided, render, and place into cell
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]);
}
}
}
// NOTE: 'table' is not passed to hz-field as hz-field is intentionally
// not cognizant of a 'table' context as hz-cell is.
html = $compile('<hz-field config="column" item="item"></hz-field>')(scope);
}
element.append(html);
}

View File

@ -12,7 +12,7 @@
<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>
<dd translate><hz-cell table="table" column="column" item="item"></hz-cell></dd>
</dl>
</span>
</div>
</div>

View File

@ -63,7 +63,7 @@
</td>
<td ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}">
<hz-cell></hz-cell>
<hz-cell table="table" column="column" item="item"></hz-cell>
</td>
<td ng-if="itemActions" class="actions_column">
<!--

View File

@ -222,6 +222,54 @@
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');
});
it('properly maps the cell content given a mapping', function() {
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1,
values: {
cat: "Catamount",
snake: "Serpent",
sparrow: "CAPTAIN Jack Sparrow"
}
},
{id: 'type', title: 'Type', priority: 2},
{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(2)').text()).toContain('Catamount');
expect($element.find('tbody tr:eq(1) td:eq(2)').text()).toContain('Serpent');
expect($element.find('tbody tr:eq(2) td:eq(2)').text()).toContain('CAPTAIN Jack Sparrow');
});
it('properly adds a link with urlFunction', function() {
$scope.config = {
selectAll: true,
expand: false,
trackId: 'id',
columns: [
{id: 'animal', title: 'Animal', priority: 1,
urlFunction: myFunction
},
{id: 'type', title: 'Type', priority: 2},
{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(2) a').attr('href')).toBe('/here/cat');
expect($element.find('tbody tr:eq(1) td:eq(2) a').attr('href')).toBe('/here/snake');
expect($element.find('tbody tr:eq(2) td:eq(2) a').attr('href')).toBe('/here/sparrow');
function myFunction(item) {
return '/here/' + item.animal;
}
});
});
});

View File

@ -23,6 +23,7 @@
'horizon.framework.widgets.details',
'horizon.framework.widgets.help-panel',
'horizon.framework.widgets.wizard',
'horizon.framework.widgets.property',
'horizon.framework.widgets.table',
'horizon.framework.widgets.modal',
'horizon.framework.widgets.modal-wait-spinner',

View File

@ -122,8 +122,7 @@
id: 'name',
priority: 1,
sortDefault: true,
template: '<a ng-href="{$ \'project/ngdetails/OS::Glance::Image/\' + item.id $}">' +
'{$ item.name $}</a>'
urlFunction: urlFunction
})
.append({
id: 'type',
@ -228,6 +227,10 @@
}
}
}
function urlFunction(item) {
return 'project/ngdetails/OS::Glance::Image/' + item.id;
}
}
/**