Manage tables data with AngularJS Part 3: configuring the fields typologies

The last part of this argument is configuring the field typologies of the tables to manage.

What you need to do is specify for one or more fields of the table a couple of informations, like the typology of the field (text, number, radio, dropdown) and perhaps a list of values.

Also we might want to pass to the field external values instead of predefined values.

In order to do that and to improve the functionalities of the application, you need to implement some new features.

FieldConfiguration table

The first step is adding a new field configuration table:

[Table("FieldConfigurations")]
public class FieldConfiguration
{
public Guid Id { get; set; }
[Required]
public string Entity { get; set; }
[Required]
public string Field { get; set; }
[Required]
public string Tipology { get; set; }
public string Values { get; set; }
}

With this table we can specify for a field the tipology (text, number, radio, dropdown) and an optional list of values.

We need also update the database and implement a Web API to retrieve the datas from the new table:

public class FieldConfigurationsController : ODataController
{
private Context db = new Context();

// GET: odata/FieldConfigurations
[EnableQuery]
public IQueryable<FieldConfiguration> GetFieldConfigurations()
{
return db.FieldConfigurations;
}

// GET: odata/FieldConfigurations(5)
[EnableQuery]
public SingleResult<FieldConfiguration> GetFieldConfiguration([FromODataUri] Guid key)
{
return SingleResult.Create(db.FieldConfigurations.Where(fieldConfiguration => fieldConfiguration.Id == key));
}

// PUT: odata/FieldConfigurations(5)
public async Task<IHttpActionResult> Put([FromODataUri] Guid key, Delta<FieldConfiguration> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

patch.Put(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!FieldConfigurationExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(fieldConfiguration);
}

// POST: odata/FieldConfigurations
public async Task<IHttpActionResult> Post(FieldConfiguration fieldConfiguration)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

fieldConfiguration.Id = Guid.NewGuid();
db.FieldConfigurations.Add(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (FieldConfigurationExists(fieldConfiguration.Id))
{
return Conflict();
}
else
{
throw;
}
}

return Created(fieldConfiguration);
}

// PATCH: odata/FieldConfigurations(5)
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] Guid key, Delta<FieldConfiguration> patch)
{
Validate(patch.GetEntity());

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

patch.Patch(fieldConfiguration);

try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!FieldConfigurationExists(key))
{
return NotFound();
}
else
{
throw;
}
}

return Updated(fieldConfiguration);
}

// DELETE: odata/FieldConfigurations(5)
public async Task<IHttpActionResult> Delete([FromODataUri] Guid key)
{
FieldConfiguration fieldConfiguration = await db.FieldConfigurations.FindAsync(key);
if (fieldConfiguration == null)
{
return NotFound();
}

db.FieldConfigurations.Remove(fieldConfiguration);
await db.SaveChangesAsync();

return StatusCode(HttpStatusCode.NoContent);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}

private bool FieldConfigurationExists(Guid key)
{
return db.FieldConfigurations.Count(e => e.Id == key) > 0;
}
}

For this implementation only reading operations are needed, but still leave the other methods.

FieldConfigurations service

We need an Angular service to consume the Web API and we use the Angular $resource module.

In addition to the CRUD methods we implement additional methods like getField, getFieldConfigurationTipology and getFieldValues, that will help to retrieve specific informations about a field.

module AngularTablesDataManagerApp.Services {
import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export interface IFieldConfigurationsResourceClass extends ngr.IResourceClass<ngr.IResource<models.IFieldConfiguration>> {
create(zip: models.IFieldConfiguration): ngr.IResource<models.IFieldConfiguration>;
}

export class FieldConfigurationsService {
private resource: IFieldConfigurationsResourceClass;
private $q: ng.IQService;
private $filter: ng.IFilterService;
private metadataService: services.MetadataService;
private entitySet: string = 'FieldConfigurations';
private fieldConfigurations: Array<models.IFieldConfiguration>;

constructor($resource: ngr.IResourceService, $q: ng.IQService, $filter: ng.IFilterService, metadataService: services.MetadataService) {
this.$q = $q;
this.$filter = $filter;
this.metadataService = metadataService;

this.resource = <IFieldConfigurationsResourceClass>$resource('/odata/' + this.entitySet + "(guid':key')", { key: '@Id' }, {
get: { method: 'GET' },
create: { method: 'POST', isArray: false, url: '/odata/' + this.entitySet },
save: { method: 'PUT' },
query: { method: 'GET', isArray: false, url: '/odata/' + this.entitySet },
delete: { method: 'DELETE' }
});
}

public create(fieldConfiguration: models.IFieldConfiguration) {
return this.resource.create(fieldConfiguration);
}

public save(fieldConfiguration: models.IFieldConfiguration) {
if (fieldConfiguration.Id == commons.Constants.GuidEmpty) {
return this.resource.create(fieldConfiguration);
}
else {
return this.resource.save(fieldConfiguration);
}
}

public delete(fieldConfiguration: models.IFieldConfiguration) {
return this.resource.delete({ key: fieldConfiguration.Id });
}

public getFieldConfigurationTipology(entityName: string, fieldName: string): ng.IPromise<string> {
var defer: ng.IDeferred<string> = this.$q.defer();

this.getFieldConfiguration(entityName, fieldName).then((data: models.IFieldConfiguration) => {
if (data != null) {
defer.resolve(data.Tipology);
}
else {
defer.resolve('');
}
}, (error) => {
defer.reject(error);
});

return defer.promise;
}

public getFieldValues(entityName: string, fieldName: string): ng.IPromise<Array<string>> {
var defer: ng.IDeferred<Array<string>> = this.$q.defer();
var vm = this;

this.getFieldConfiguration(entityName, fieldName).then((data: models.IFieldConfiguration) => {
if (data != null && data.Values != null && data.Values != "") {
defer.resolve(data.Values.split(";"));
}
else {
defer.resolve(new Array<string>());
}

}, (error) => {
defer.reject(error);
});

if (this.fieldConfigurations) {

}
else {

}

return defer.promise;
}

private getFieldConfiguration(entityName: string, fieldName: string): ng.IPromise<models.IFieldConfiguration> {
var defer: ng.IDeferred<models.IFieldConfiguration> = this.$q.defer();
var vm = this;

if (this.fieldConfigurations == null) {
vm.resource.query().$promise.then((data: any) => {
vm.fieldConfigurations = data["value"];
defer.resolve(vm.getField(entityName, fieldName));
}, (error) => {
defer.reject(error);
});
}
else {
defer.resolve(vm.getField(entityName, fieldName));
}

return defer.promise;
}

private getField(entityName: string, fieldName: string): models.IFieldConfiguration {
var fieldConfigurations: Array<models.IFieldConfiguration> = this.$filter('filter')(this.fieldConfigurations, { 'Entity': entityName, 'Field': fieldName }, true);
if (fieldConfigurations.length > 0) {
return fieldConfigurations[0];
}
else {
return null;
}
}

static factory() {
return (r: ngr.IResourceService, $q: ng.IQService, $filter: ng.IFilterService, MetadataService: services.MetadataService) => new FieldConfigurationsService(r, $q, $filter, MetadataService);
}
}

AngularTablesDataManager.module.factory('FieldConfigurationsService', ['$resource', '$q', '$filter', 'MetadataService', FieldConfigurationsService.factory()]);
}

The getFieldConfiguration method check if the configurations aren’t already loaded and if not invoke the Web API.

Field directive

Now you can implement the directive that will manage the fields visualization.

The directive accept some parameters, like the tipology of the field, the property object that you want to show and the possible values of the field:

module AngularTablesDataManagerApp.Directives {
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

interface IFieldDirectiveScope extends ng.IScope {
entityName: string;
contentUrl: string;
tipology: models.MetadataProperty;
property: models.RowProperty;
fieldItems: Array<models.FieldItem>;
values: Array<models.FieldItem>;
}

export class FieldDirective implements ng.IDirective {
fieldConfigurationsService: services.FieldConfigurationsService;
public restrict = 'E';
public scope = {
entityName: '=',
tipology: '=',
property: '=',
fieldItems: '='
};
public template = '<ng-include src="contentUrl" />';
public link = (scope: IFieldDirectiveScope, element: JQuery, attrs: IArguments) => {
scope.values = new Array<models.FieldItem>();

if (scope.fieldItems != null) {
for (var i = 0; i < scope.fieldItems.length; i++) {
scope.values.push(scope.fieldItems[i]);
}
}

var tipology: string = scope.tipology.Type;
this.fieldConfigurationsService.getFieldConfigurationTipology(scope.entityName, scope.tipology.Name).then((data: string) => {
if (data != "") {
tipology = data;
this.fieldConfigurationsService.getFieldValues(scope.entityName, scope.tipology.Name).then((data: Array<string>) => {
for (var i = 0; i < data.length; i++) {
var item: models.FieldItem = new models.FieldItem(data[i], data[i]);
scope.values.push(item);
}

this.openView(scope, tipology);
});
}
else {
this.openView(scope, tipology);
}
});
}

public constructor(fieldConfigurationService: services.FieldConfigurationsService) {
this.fieldConfigurationsService = fieldConfigurationService;
}

private openView(scope: IFieldDirectiveScope, tipology: string) {
if (tipology.toLowerCase().indexOf('int') != -1) {
scope.contentUrl = 'app/directives/InputNumber.html';
}
else if (tipology.toLowerCase().indexOf('radio') != -1) {
scope.contentUrl = 'app/directives/InputRadio.html'
}
else if (tipology.toLowerCase().indexOf('select') != -1) {
scope.contentUrl = 'app/directives/InputSelect.html';
}
else {
scope.contentUrl = 'app/directives/InputText.html';
}
}
}

AngularTablesDataManager.module.directive('field', ['FieldConfigurationsService', (fieldConfigurationService: services.FieldConfigurationsService) => new Directives.FieldDirective(fieldConfigurationService)]);
}

In the link function you need to call the service to retrieve the tipology of the field and to check if some values are defined.

An openView method check the field tipology and open the specific view; for example, the InputSelect view will look like this:

<select class="form-control" ng-model="property.Value">
<option ng-repeat="value in values | orderBy: 'Value'" value="{{value.Id}}" ng-selected="value.Id == property.Value">{{value.Value}}</option>
</select>

 

Grid directive

You need to extend the GridController and adding some methods to retrive the necessary informations and pass that to the Field directive:

interface IGridDirectiveScope extends ng.IScope {
entityName: string;
list: models.Grid;
item: models.Row;
rowModel: models.Row;
newItem: boolean;
fieldItems: Array<models.FieldItems>;

New(): void;
Save(item: IGridItem): void;
Delete(item: IGridItem): void;
Close(): void;
GetEntityName(): string;
GetFieldItems(fieldName: string): Array<models.FieldItem>;
IsVisiblePropertyInGrid(property: models.RowProperty): boolean;
IsVisiblePropertyInDetail(fieldName: string): boolean;
}

class GridController {
$scope: IGridDirectiveScope;
$filter: ng.IFilterService;

constructor($scope: IGridDirectiveScope, $filter: ng.IFilterService) {
this.$scope = $scope;
this.$filter = $filter;
}

public Edit(item: models.Row) {
this.$scope.item = item;
this.$scope.newItem = false;
}

public New() {
this.$scope.item = new models.Row(angular.copy(this.$scope.rowModel.Entity), this.$scope.rowModel.Name, angular.copy(this.$scope.rowModel.Properties));
this.$scope.newItem = true;
}

public Save(item: models.Row) {
var obj: IGridItem = { item: item };

this.$scope.Save(obj);
}

public Delete(item: models.Row) {
this.$scope.item = null;
var obj: IGridItem = { item: item };

this.$scope.Delete(obj);
}

public Close() {
if (!this.$scope.newItem) {
for (var i = 0; i < this.$scope.item.Properties.length; i++) {
this.$scope.item.Properties[i].Value = (<any>this.$scope.item.Entity)[this.$scope.item.Properties[i].Name];
}
}

this.$scope.item = null;
}

public GetEntityName() {
return this.$scope.entityName;
}

public GetFieldItems(fieldName: string) {
if (this.$filter('filter')(this.$scope.fieldItems, { 'FieldName': fieldName }, true).length > 0) {
return this.$filter('filter')(this.$scope.fieldItems, { 'FieldName': fieldName }, true)[0].FieldItems;
}
else {
return null;
}
}

public IsVisiblePropertyInGrid(property: models.RowProperty) {
return this.$filter('filter')(this.$scope.list.Columns, { 'Name': property.Name }, true)[0].ShowedInGrid;
}

public IsVisiblePropertyInDetail(property: models.RowProperty) {
return this.$filter('filter')(this.$scope.list.Columns, { 'Name': property.Name }, true)[0].ShowedInDetail;
}
}

A particular aspect is the fieldItems property, that is an array of objects; if you want to pass an array of values to the field directive, what we need to do is load these values in the caller of the directive and pass these as a parameter.

Now you need to change the GridItem view and use the FieldDirective:

<div class="row bootstrap-admin-no-edges-padding" style="margin-top: 10px;">
<div class="col-md-12">
<input type="button" id="btn-save" value="Save" class="btn btn-primary" ng-click="Save()" ng-disabled="form.$invalid" />
<input type="button" id="btn-delete" value="Delete" class="btn btn-danger" ng-click="Delete()" ng-show="!isNew" />
<input type="button" id="btn-close" value="Close" class="btn btn-default" ng-click="Close()" /></div>
</div>
<div class="row bootstrap-admin-no-edges-padding" style="margin-top: 10px;">
<div class="col-md-12">
<form name="form" role="form">
<div class="panel panel-default">
<div class="panel-heading">
<div class="text-muted bootstrap-admin-box-title">
{{item.Name}}</div>
</div>
<div class="panel-body">
<div class="form-group" ng-repeat="property in item.Properties | filter: IsVisibleProperty">
<label for="{{property.Name}}">{{property.Name}}:</label>
<field entity-name="GetEntityName()" tipology="GetMetadataProperty(property.Name)" property="property" field-items="GetFieldItems(property.Name)" /></div>
</div>
<div class="col-md-2"></div>
</div>
</form></div>
</div>

Cities controller

The last step is changing the main controller to retrive the new parameters that the GridDirective needed:

module AngularTablesDataManagerApp.Controllers {
import ngr = ng.resource;
import commons = AngularTablesDataManagerApp.Commons;
import models = AngularTablesDataManagerApp.Models;
import services = AngularTablesDataManagerApp.Services;

export class CitiesController {
entityName: string;
grid: models.Grid;
fieldItems: Array<models.FieldItems>;
rowModel: models.Row;
toaster: ngtoaster.IToasterService;

private citiesService: services.CitiesService;
private zipsService: services.ZipsService;
private constant: commons.Constants;

constructor(toaster: ngtoaster.IToasterService, CitiesService: services.CitiesService, ZipsService: services.ZipsService) {
this.citiesService = CitiesService;
this.zipsService = ZipsService;
this.constant = commons.Constants;
this.toaster = toaster;
this.entityName = this.citiesService.entityName;

this.grid = new models.Grid();
this.fieldItems = new Array<models.FieldItems>();
this.grid.Title = 'Cities';
this.Load();
}

private Load() {
var columns: Array<models.Column> = new Array<models.Column>();
var column: models.Column = new models.Column('Name', true, true);
columns.push(column);
column = new models.Column('IdZip', false, true);
columns.push(column);
var vm = this;

this.zipsService.getAll().then((data) => {
var zips: models.FieldItems = new models.FieldItems('IdZip');

for (var i = 0; i < data.length; i++) {
zips.FieldItems.push(new models.FieldItem(data[i].Id, data[i].Code.toString()));
}

vm.fieldItems.push(zips);

vm.citiesService.getMetadata(columns).then((data) => {
vm.grid.Columns = data;
vm.rowModel = this.citiesService.createGridData(data);

vm.citiesService.getGridData(data).then((data) => {
vm.grid.Rows = data;
vm.toaster.success('Cities loaded successfully.');
return;
}, (error) => {
vm.toaster.error('Error loading cities', error.message);
});

}, (error) => {
vm.toaster.error('Error loading cities metadata', error.data.message);
});

}, (error) => {
vm.toaster.error('Error loading zips metadata', error.data.message);
});

}

public Save(item: models.Row) {
var vm = this;
var isNew: boolean = false;

if (item.Entity.Id == commons.Constants.GuidEmpty)
isNew = true;

this.citiesService.saveGridData(item).then((data: models.Row) => {
if (isNew)
vm.grid.Rows.push(data);

this.toaster.success("City saved successfully.");
}, (error: any) => {
this.toaster.error("Error saving city", error.data.message);
});
}

public Delete(item: models.Row) {
var vm = this;
this.citiesService.deleteGridData(item).then((data: any) => {
var index = vm.grid.Rows.indexOf(item);
vm.grid.Rows.splice(index, 1);

this.toaster.success("City deleted successfully.");
}, (error: any) => {
this.toaster.error("Error deleting city", error.data.message);
});
}
}

AngularTablesDataManager.module.controller('CitiesController', CitiesController);
}

The items to pass to the GridController are an array of FieldItems; an object FieldItems has a FieldName that is the field to witch to associate the values and an array of FieldItem that are the effective values.

Using the ZipService we build an array of FieldItem associated to the IdZip field, and we’re passing that to the GridDirective.

The view is modified like this:

<grid list="vm.grid" entity-name="vm.entityName" row-model="vm.rowModel" order="Name" save="vm.Save(item)" delete="vm.Delete(item)" field-items="vm.fieldItems"></grid>

 

You can find the source code here.

 

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: