Simplified form
Change-Id: Ib4b17823be86e18bd5e83679cde7fc95a4f8bac1
diff --git a/src/app/core/form/form-helpers.ts b/src/app/core/form/form-helpers.ts
index 0677cbd..d2d7373 100644
--- a/src/app/core/form/form-helpers.ts
+++ b/src/app/core/form/form-helpers.ts
@@ -1,10 +1,11 @@
-import * as _ from 'lodash';
import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
+import {IXosFormInput} from './form';
export interface IXosFormHelpersService {
_getFieldFormat(value: any): string;
parseModelField(fields: any): any[];
buildFormStructure(modelField: any[], customField: any[], model: any, order: string[]): any;
+ buildFormData(fields: IXosFormInput[], model: any): any;
}
export class XosFormHelpers {
@@ -54,53 +55,5 @@
return typeof value;
};
-
- public buildFormStructure = (modelField, customField, model, order) => {
- // TODO take an array as input
- // NOTE do we want to support auto-generated forms??
- // We can take that out of this component and autogenerate the config somewhere else
- const orderedForm = {};
-
- modelField = angular.extend(modelField, customField);
- customField = customField || {};
-
- if (order) {
- _.each(order, function (key: string) {
- orderedForm[key] = {};
- });
- }
-
- _.each(Object.keys(modelField), (f) => {
-
- orderedForm[f] = {
- label: (customField[f] && customField[f].label) ? `${customField[f].label}:` : this.ConfigHelpers.toLabel(f),
- type: (customField[f] && customField[f].type) ? customField[f].type : this._getFieldFormat(model[f]),
- validators: (customField[f] && customField[f].validators) ? customField[f].validators : {},
- hint: (customField[f] && customField[f].hint) ? customField[f].hint : '',
- };
-
- if (customField[f] && customField[f].options) {
- orderedForm[f].options = customField[f].options;
- }
- if (customField[f] && customField[f].properties) {
- orderedForm[f].properties = customField[f].properties;
- }
- if (orderedForm[f].type === 'date') {
- model[f] = new Date(model[f]);
- }
-
- if (orderedForm[f].type === 'number') {
- model[f] = parseInt(model[f], 10);
- }
- });
-
- return orderedForm;
- };
-
- public parseModelField = (fields) => {
- return _.reduce(fields, (form, f) => {
- form[f] = {};
- return form;
- }, {});
}
-}
+
diff --git a/src/app/core/form/form.html b/src/app/core/form/form.html
index a583269..2172d3a 100644
--- a/src/app/core/form/form.html
+++ b/src/app/core/form/form.html
@@ -1,8 +1,11 @@
<form name="vm.{{vm.config.formName || 'form'}}" novalidate>
- <div class="form-group" ng-repeat="(name, field) in vm.formField">
- <xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>
- <xos-validation field="vm[vm.config.formName || 'form'][name]" form = "vm[vm.config.formName || 'form']"></xos-validation>
- <div class="alert alert-info" ng-show="(field.hint).length >0" role="alert">{{field.hint}}</div>
+ <!--<div class="form-group" ng-repeat="(name, field) in vm.formField">-->
+ <!--<xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>-->
+ <!--<xos-validation field="vm[vm.config.formName || 'form'][name]" form = "vm[vm.config.formName || 'form']"></xos-validation>-->
+ <!--<div class="alert alert-info" ng-show="(field.hint).length >0" role="alert">{{field.hint}}</div>-->
+ <!--</div>-->
+ <div class="form-group" ng-repeat="field in vm.config.inputs">
+ <xos-field name="field.name" field="field" ng-model="vm.ngModel[field.name]"></xos-field>
</div>
<div class="form-group" ng-if="vm.config.actions">
<!--<xos-alert config="vm.config.feedback" show="vm.config.feedback.show">{{vm.config.feedback.message}}</xos-alert>-->
@@ -15,4 +18,4 @@
{{action.label}}
</button>
</div>
-</form>
\ No newline at end of file
+</form>
diff --git a/src/app/core/form/form.ts b/src/app/core/form/form.ts
index a5cfa67..6713fe7 100644
--- a/src/app/core/form/form.ts
+++ b/src/app/core/form/form.ts
@@ -4,6 +4,45 @@
import {IXosFormHelpersService} from './form-helpers';
import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
+export interface IXosFormAction {
+ label: string;
+ icon: string;
+ class: string;
+ cb(item: any, form: any): void;
+}
+
+export interface IXosFeedback {
+ show: boolean;
+ message: string;
+ type: string; // NOTE is possible to enumerate success, error, warning, info?
+}
+
+export interface IXosFormInputValidator {
+ minlength?: number;
+ maxlength?: number;
+ required?: boolean;
+ min?: number;
+ max?: number;
+ custom?(value: any): any;
+ // do your validation here and return true | false
+ // alternatively you can return an array [errorName, true|false]
+}
+
+export interface IXosFormInput {
+ name: string;
+ label: string;
+ type: string; // options are: [date, boolean, number, email, string, select],
+ validators: IXosFormInputValidator;
+}
+
+export interface IXosFormConfig {
+ exclude?: string[];
+ actions: IXosFormAction[];
+ feedback?: IXosFeedback;
+ inputs: IXosFormInput[];
+ formName: string;
+}
+
class FormCtrl {
$inject = ['$onInit', '$scope', 'XosFormHelpers', 'ConfigHelpers'];
@@ -17,7 +56,6 @@
private XosFormHelpers: IXosFormHelpersService,
private ConfigHelpers: IXosConfigHelpersService
) {
-
}
$onInit() {
@@ -29,6 +67,11 @@
throw new Error('[xosForm] Please provide an action list in the configuration');
}
+ if (!this.config.formName) {
+ throw new Error('[xosForm] Please provide a formName property in the config');
+ }
+
+ // NOTE is needed ??
if (!this.config.feedback) {
this.config.feedback = {
show: false,
@@ -37,36 +80,8 @@
};
}
- // TODO Define this list in a service (eg: ConfigHelper)
- this.excludedField = this.ConfigHelpers.excluded_fields;
- if (this.config && this.config.exclude) {
- this.excludedField = this.excludedField.concat(this.config.exclude);
- }
-
- this.formField = [];
-
- this.$scope.$watch(() => this.config, () => {
- if (!this.ngModel) {
- return;
- }
- let diff = _.difference(Object.keys(this.ngModel), this.excludedField);
- let modelField = this.XosFormHelpers.parseModelField(diff);
- this.formField = this.XosFormHelpers.buildFormStructure(modelField, this.config.fields, this.ngModel, this.config.order);
- }, true);
-
- this.$scope.$watch(() => this.ngModel, (model) => {
- console.log(this.ngModel);
- // empty from old stuff
- this.formField = {};
- if (!model) {
- return;
- }
- let diff = _.difference(Object.keys(model), this.excludedField);
- let modelField = this.XosFormHelpers.parseModelField(diff);
- this.formField = this.XosFormHelpers.buildFormStructure(modelField, this.config.fields, model, this.config.order);
- console.log(this.formField);
- });
-
+ // remove excluded inputs
+ _.remove(this.config.inputs, i => this.config.exclude.indexOf(i.name) > -1);
}
}
diff --git a/src/app/core/services/helpers/config.helpers.spec.ts b/src/app/core/services/helpers/config.helpers.spec.ts
index 432bd7e..beec2e0 100644
--- a/src/app/core/services/helpers/config.helpers.spec.ts
+++ b/src/app/core/services/helpers/config.helpers.spec.ts
@@ -6,6 +6,7 @@
import {xosCore} from '../../index';
import {IModeldef} from '../../../datasources/rest/modeldefs.rest';
import {IXosTableCfg} from '../../table/table';
+import {IXosFormInput, IXosFormConfig} from '../../form/form';
let service: IXosConfigHelpersService;
@@ -20,12 +21,24 @@
{
type: 'string',
name: 'name',
- validators: {}
+ validators: {
+ required: true
+ }
},
{
type: 'string',
name: 'something',
- validators: {}
+ validators: {
+ maxlength: 30
+ }
+ },
+ {
+ type: 'number',
+ name: 'else',
+ validators: {
+ min: 20,
+ max: 40
+ }
},
{
type: 'date',
@@ -51,6 +64,7 @@
expect(service.pluralize('test', 1)).toEqual('test');
expect(service.pluralize('xos')).toEqual('xosses');
expect(service.pluralize('slice')).toEqual('slices');
+ expect(service.pluralize('Slice', 1)).toEqual('Slice');
});
it('should preprend count to string', () => {
@@ -104,7 +118,11 @@
expect(cols[2].prop).toBe('something');
expect(cols[2].link).not.toBeDefined();
- expect(cols[3]).not.toBeDefined();
+ expect(cols[3].label).toBe('Else');
+ expect(cols[3].prop).toBe('else');
+ expect(cols[3].link).not.toBeDefined();
+
+ expect(cols[4]).not.toBeDefined();
});
});
@@ -118,8 +136,47 @@
});
});
+ describe('the modelFieldToInputConfig', () => {
+ it('should return an array of inputs', () => {
+ const inputs: IXosFormInput[] = service.modelFieldToInputCfg(model.fields);
+ expect(inputs[0].name).toBe('id');
+ expect(inputs[0].type).toBe('number');
+ expect(inputs[0].label).toBe('Id');
+
+ expect(inputs[1].name).toBe('name');
+ expect(inputs[1].type).toBe('string');
+ expect(inputs[1].label).toBe('Name');
+ expect(inputs[1].validators.required).toBe(true);
+
+ expect(inputs[2].name).toBe('something');
+ expect(inputs[2].type).toBe('string');
+ expect(inputs[2].label).toBe('Something');
+ expect(inputs[2].validators.maxlength).toBe(30);
+
+ expect(inputs[3].name).toBe('else');
+ expect(inputs[3].type).toBe('number');
+ expect(inputs[3].label).toBe('Else');
+ expect(inputs[3].validators.min).toBe(20);
+ expect(inputs[3].validators.max).toBe(40);
+ });
+ });
+
+ describe('the modelToFormCfg method', () => {
+ it('should return a form config', () => {
+ const config: IXosFormConfig = service.modelToFormCfg(model);
+ expect(config.formName).toBe('TestForm');
+ expect(config.actions.length).toBe(1);
+ expect(config.actions[0].label).toBe('Save');
+ expect(config.actions[0].class).toBe('success');
+ expect(config.actions[0].icon).toBe('ok');
+ expect(config.actions[0].cb).toBeDefined();
+ expect(config.inputs.length).toBe(4);
+ });
+ });
+
it('should convert a core model name in an URL', () => {
expect(service.urlFromCoreModel('Slice')).toBe('/core/slices');
expect(service.urlFromCoreModel('Xos')).toBe('/core/xosses');
});
});
+
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index fc836e7..6b89856 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -2,6 +2,7 @@
import * as pluralize from 'pluralize';
import {IXosTableColumn, IXosTableCfg} from '../../table/table';
import {IModeldef} from '../../../datasources/rest/modeldefs.rest';
+import {IXosFormConfig, IXosFormInput} from '../../form/form';
export interface IXosModelDefsField {
name: string;
@@ -11,8 +12,10 @@
export interface IXosConfigHelpersService {
excluded_fields: string[];
- modelToTableCfg(model: IModeldef, baseUrl: string): IXosTableCfg;
modelFieldsToColumnsCfg(fields: IXosModelDefsField[], baseUrl: string): IXosTableColumn[]; // TODO use a proper interface
+ modelToTableCfg(model: IModeldef, baseUrl: string): IXosTableCfg;
+ modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[];
+ modelToFormCfg(model: IModeldef): IXosFormConfig;
pluralize(string: string, quantity?: number, count?: boolean): string;
toLabel(string: string, pluralize?: boolean): string;
toLabels(string: string[], pluralize?: boolean): string[];
@@ -35,7 +38,9 @@
'omf_friendly',
'enabled',
'validators',
- 'password'
+ 'password',
+ 'backend_need_delete',
+ 'backend_need_reap'
];
constructor(
@@ -43,6 +48,7 @@
) {
pluralize.addIrregularRule('xos', 'xosses');
pluralize.addPluralRule(/slice$/i, 'slices');
+ pluralize.addSingularRule(/slice$/i, 'slice');
}
public pluralize(string: string, quantity?: number, count?: boolean): string {
@@ -143,6 +149,41 @@
return `/core/${this.pluralize(name.toLowerCase())}`;
}
+ public modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[] {
+
+ return _.map(fields, f => {
+ return {
+ name: f.name,
+ label: this.toLabel(f.name),
+ type: f.type,
+ validators: f.validators
+ };
+ })
+ .filter(f => this.excluded_fields.indexOf(f.name) === -1);
+ }
+
+ public modelToFormCfg(model: IModeldef): IXosFormConfig {
+ return {
+ formName: `${model.name}Form`,
+ exclude: ['backend_status'],
+ actions: [{
+ label: 'Save',
+ class: 'success',
+ icon: 'ok',
+ cb: (item, form) => {
+ item.$save()
+ .then(res => {
+ this.toastr.success(`${item.name} succesfully saved`);
+ })
+ .catch(err => {
+ this.toastr.error(`Error while saving ${item.name}`);
+ });
+ }
+ }],
+ inputs: this.modelFieldToInputCfg(model.fields)
+ };
+ }
+
private fromCamelCase(string: string): string {
return string.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join(' ');
}
@@ -159,4 +200,3 @@
return string.slice(0, 1).toUpperCase() + string.slice(1);
}
}
-
diff --git a/src/app/views/crud/crud.html b/src/app/views/crud/crud.html
index 7c80b92..f5f1f76 100644
--- a/src/app/views/crud/crud.html
+++ b/src/app/views/crud/crud.html
@@ -5,6 +5,7 @@
<div class="view-header">
<div class="pull-right text-right" style="line-height: 14px">
<!--<small>UI Elements<br>General<br> <span class="c-white">Grid system</span></small>-->
+ <a class="btn btn-default" ng-if="vm.list" href="{{vm.baseUrl}}add">Add</a>
<a class="btn btn-default" ng-if="!vm.list" href="{{vm.baseUrl}}">Back</a>
</div>
<div class="header-icon">
@@ -20,7 +21,7 @@
<hr>
</div>
</div>
- <div class="row" ng-show="vm.related.length > 0">
+ <div class="row" ng-show="vm.related.length > 0 && vm.model.id">
<div class="view-header">
<div class="col-lg-4">
<h4>Related Items: </h4>
@@ -47,4 +48,4 @@
<xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
</div>
</div>
-</section>
\ No newline at end of file
+</section>
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 1d4eccd..f3cc1fa 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -2,14 +2,17 @@
import {IModelStoreService} from '../../datasources/stores/model.store';
import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
import * as _ from 'lodash';
+import {IXosFormConfig} from '../../core/form/form';
+import {IXosResourceService} from '../../datasources/rest/model.rest';
export interface IXosCrudData {
model: string;
related: string[];
xosTableCfg: IXosTableCfg;
+ xosFormCfg: IXosFormConfig;
}
class CrudController {
- static $inject = ['$scope', '$state', '$stateParams', 'ModelStore', 'ConfigHelpers'];
+ static $inject = ['$scope', '$state', '$stateParams', 'ModelStore', 'ConfigHelpers', 'ModelRest'];
public data: IXosCrudData;
public tableCfg: IXosTableCfg;
@@ -27,7 +30,8 @@
private $state: angular.ui.IStateService,
private $stateParams: ng.ui.IStateParamsService,
private store: IModelStoreService,
- private ConfigHelpers: IXosConfigHelpersService
+ private ConfigHelpers: IXosConfigHelpersService,
+ private ModelRest: IXosResourceService
) {
this.data = this.$state.current.data;
this.tableCfg = this.data.xosTableCfg;
@@ -39,19 +43,7 @@
this.related = $state.current.data.related;
- this.formCfg = {
- formName: 'sampleForm',
- actions: [
- {
- label: 'Save',
- icon: 'ok', // refers to bootstraps glyphicon
- cb: (item) => { // receive the model
- console.log(item);
- },
- class: 'success'
- }
- ]
- };
+ this.formCfg = $state.current.data.xosFormCfg;
this.store.query(this.data.model)
.subscribe(
@@ -61,8 +53,8 @@
this.title = this.ConfigHelpers.pluralize(this.data.model, event.length);
this.tableData = event;
- // if it is a detail page
- if ($stateParams['id']) {
+ // if it is a detail page for an existing model
+ if ($stateParams['id'] && $stateParams['id'] !== 'add') {
this.model = _.find(this.tableData, {id: parseInt($stateParams['id'], 10)});
}
});
@@ -72,11 +64,19 @@
// if it is a detail page
if ($stateParams['id']) {
this.list = false;
+
+ // if it is the create page
+ if ($stateParams['id'] === 'add') {
+ // generate a resource for an empty model
+ const endpoint = this.ConfigHelpers.urlFromCoreModel(this.data.model);
+ const resource = this.ModelRest.getResource(endpoint);
+ this.model = new resource({});
+ }
}
}
public getRelatedItem(relation: string, item: any): number {
- if (angular.isDefined(item[relation.toLowerCase()])) {
+ if (item && angular.isDefined(item[relation.toLowerCase()])) {
return item[relation.toLowerCase()];
}
return 0;
@@ -88,3 +88,4 @@
controllerAs: 'vm',
controller: CrudController
};
+
diff --git a/src/index.ts b/src/index.ts
index 2a3129a..ca6ca34 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -78,7 +78,7 @@
// Dinamically add a core states
ModelDefs.get()
.then((models: IModeldef[]) => {
- // TODO move in a separate service and test
+ // TODO move in a separate service and test (StateConfig service in Core)
_.forEach(models, (m: IModeldef) => {
const stateUrl = `/${ConfigHelpers.pluralize(m.name.toLowerCase())}/:id?`;
const stateName = `xos.core.${ConfigHelpers.pluralize(m.name.toLowerCase())}`;
@@ -92,8 +92,8 @@
data: {
model: m.name,
related: m.relations,
- xosTableCfg: ConfigHelpers.modelToTableCfg(m, stateUrl)
- // TODO add form config
+ xosTableCfg: ConfigHelpers.modelToTableCfg(m, stateUrl),
+ xosFormCfg: ConfigHelpers.modelToFormCfg(m)
}
};
@@ -109,3 +109,4 @@
$location.path(lastRoute);
});
});
+