Add form submission, validation, rejection messaging

This commit is contained in:
gconsidine 2017-05-23 16:38:58 -04:00
parent c41dff7996
commit 19fa782fb4
21 changed files with 252 additions and 121 deletions

View File

@ -3,24 +3,26 @@ function AddCredentialsController (models) {
let credential = models.credential;
let credentialType = models.credentialType;
vm.name = credential.getPostOptions('name');
vm.description = credential.getPostOptions('description');
vm.kind = Object.assign({
data: credentialType.categorizeByKind(),
placeholder: 'Select a Type'
}, credential.getPostOptions('credential_type'));
vm.form = credential.createFormSchema('post', {
omit: ['user', 'team', 'inputs']
});
vm.dynamic = {
getInputs: credentialType.getTypeFromName,
source: vm.kind,
reference: 'vm.dynamic'
vm.form.credential_type.data = credentialType.categorizeByKind();
vm.form.credential_type.placeholder = 'Select A Type';
vm.form.inputs = {
get: credentialType.getTypeFromName,
source: vm.form.credential_type,
reference: 'vm.form.inputs',
key: 'inputs'
};
vm.form.save = credential.post;
}
AddCredentialsController.$inject = [
'credentialType'
'resolvedModels'
];
export default AddCredentialsController;

View File

@ -7,12 +7,12 @@
</at-tab-navigation>
<at-panel-body>
<at-form>
<at-input-text col="4" tab="1" state="vm.name"></at-input-text>
<at-input-text col="4" tab="2" state="vm.description"></at-input-text>
<at-input-select col="4" tab="3" state="vm.kind"></at-input-select>
<at-form state="vm.form">
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
<at-input-select col="4" tab="3" state="vm.form.credential_type"></at-input-select>
<at-dynamic-input-group col="4" tab="4" state="vm.dynamic">
<at-dynamic-input-group col="4" tab="4" state="vm.form.inputs">
Type Details
</at-dynamic-input-group>

View File

@ -40,7 +40,7 @@ function config ($stateExtenderProvider, pathServiceProvider) {
}
});
function credentialTypeResolve ($q, credentialModel, credentialTypeModel) {
function CredentialsAddResolve ($q, credentialModel, credentialTypeModel) {
let promises = [
credentialModel.options(),
credentialTypeModel.get()
@ -53,7 +53,7 @@ function config ($stateExtenderProvider, pathServiceProvider) {
}));
}
credentialTypeResolve.$inject = ['$q', 'CredentialModel', 'CredentialTypeModel'];
CredentialsAddResolve.$inject = ['$q', 'CredentialModel', 'CredentialTypeModel'];
stateExtender.addState({
name: 'credentials.add',
@ -69,7 +69,7 @@ function config ($stateExtenderProvider, pathServiceProvider) {
}
},
resolve: {
credentialType: credentialTypeResolve
resolvedModels: CredentialsAddResolve
}
});

View File

@ -1,7 +1,6 @@
@import 'action/_index';
@import 'badge/_index';
@import 'dynamic/_index';
@import 'form/_index';
@import 'input/_index';
@import 'panel/_index';
@import 'popover/_index';

View File

@ -44,7 +44,7 @@ function AtDynamicInputGroupController ($scope, $compile) {
state.value = source.value;
let inputs = state.getInputs(source.value);
let inputs = state.get(source.value);
let components = vm.createComponentConfigs(inputs);
vm.insert(components);
@ -70,6 +70,7 @@ function AtDynamicInputGroupController ($scope, $compile) {
components.push(Object.assign({
element: vm.createElement(input, i),
key: 'inputs',
dynamic: true
}, input));
});

View File

@ -1,5 +1,5 @@
<button class="btn at-Button{{ fill }}--{{ color }}"
ng-disabled="type !== 'cancel' && !form.isValid"
ng-class="{ 'at-Button--disabled': !form.isValid }" ng-click="action()">
ng-disabled="form.disabled || (type === 'save' && !form.isValid)"
ng-click="action()">
{{::text}}
</button>

View File

@ -13,7 +13,9 @@ function AtFormController (eventService) {
vm.components = [];
vm.state = {
isValid: false
isValid: false,
disabled: false,
value: {}
};
vm.init = (_scope_, _form_) => {
@ -27,10 +29,6 @@ function AtFormController (eventService) {
component.category = category;
component.form = vm.state;
if (category === 'input') {
component.state.index = vm.components.length;
}
vm.components.push(component)
};
@ -43,13 +41,12 @@ function AtFormController (eventService) {
};
vm.submitOnEnter = event => {
if (event.key !== 'Enter') {
if (event.key !== 'Enter' || event.srcElement.type === 'textarea') {
return;
}
event.preventDefault();
vm.submit();
scope.$apply(vm.submit);
};
vm.submit = event => {
@ -57,7 +54,58 @@ function AtFormController (eventService) {
return;
}
console.log('submit', event, vm.components);
vm.state.disabled = true;
let data = vm.components
.filter(component => component.category === 'input')
.reduce((values, component) => {
if (!component.state.value) {
return values;
}
if (component.state.dynamic) {
values[component.state.key] = values[component.state.key] || [];
values[component.state.key].push({
[component.state.id]: component.state.value
});
} else {
values[component.state.id] = component.state.value;
}
return values;
}, {});
scope.state.save(data)
.then(res => vm.onSaveSuccess(res))
.catch(err => vm.onSaveError(err))
.finally(() => vm.state.disabled = false);
};
vm.onSaveSuccess = res => {
console.info(res);
};
vm.onSaveError = err => {
if (err.status === 400) {
vm.setValidationErrors(err.data);
}
};
vm.setValidationErrors = errors => {
for (let id in errors) {
vm.components
.filter(component => component.category === 'input')
.forEach(component => {
if (component.state.id === id) {
component.state.rejected = true;
component.state.isValid = false;
component.state.message = errors[id].join(' ');
}
});
}
vm.check();
};
vm.validate = () => {
@ -86,12 +134,14 @@ function AtFormController (eventService) {
};
vm.deregisterDynamicComponents = components => {
let offset = 0;
components.forEach(component => {
vm.components.splice(component.index - offset, 1);
offset++;
});
for (let i = 0; i < components.length; i++) {
for (let j = 0; j < vm.components.length; j++) {
if (components[i] === vm.components[j].state) {
vm.components.splice(j, 1);
break;
}
}
}
};
}

View File

@ -18,7 +18,6 @@ import toggleContent from './toggle/content.directive';
import BaseInputController from './input/base.controller';
angular
.module('at.lib.components', [])
.directive('atActionGroup', actionGroup)

View File

@ -19,7 +19,10 @@
border-color: @at-blue;
}
.at-InputLabel {
.at-Input--rejected {
&, &:focus {
border-color: @at-red;
}
}
.at-InputLabel-name {
@ -77,3 +80,10 @@
background-color: @at-white;
}
}
.at-InputMessage--rejected {
font-size: @at-font-size;
color: @at-red;
margin: @at-space-3x 0 0 0;
padding: 0;
}

View File

@ -1,3 +1,6 @@
const REQUIRED_INPUT_MISSING_MESSAGE = 'Please enter a value.';
const DEFAULT_INVALID_INPUT_MESSAGE = 'Invalid input for this type.';
function BaseInputController () {
return function extend (type, scope, element, form) {
let vm = this;
@ -12,23 +15,36 @@ function BaseInputController () {
vm.validate = () => {
let isValid = true;
let message = '';
if (scope.state.required && !scope.state.value) {
isValid = false;
message = REQUIRED_INPUT_MISSING_MESSAGE;
}
if (scope.state.validate && !scope.state.validate(scope.state.value)) {
isValid = false;
if (scope.state.validate) {
let result = scope.state.validate(scope.state.value);
if (!result.isValid) {
isValid = false;
message = result.message || DEFAULT_INVALID_INPUT_MESSAGE;
}
}
return isValid;
return {
isValid,
message
};
};
vm.check = () => {
let isValid = vm.validate();
let result = vm.validate();
if (result.isValid !== scope.state.isValid) {
scope.state.rejected = !result.isValid;
scope.state.isValid = result.isValid;
scope.state.message = result.message;
if (isValid !== scope.state.isValid) {
scope.state.isValid = isValid;
form.check();
}
};

View File

@ -3,15 +3,21 @@
<at-input-label state="state"></at-input-label>
<div class="input-group">
<span class="input-group-btn">
<button class="btn at-ButtonHollow--white at-Input-button" ng-click="vm.toggle()">
<button class="btn at-ButtonHollow--white at-Input-button"
ng-disabled="state.disabled || form.disabled" ng-click="vm.toggle()">
{{ buttonText }}
</button>
</span>
<input type="{{ type }}" class="form-control at-Input" ng-model="state.value"
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state.placeholder || undefined }}"
ng-change="vm.check()" ng-disabled="state.disabled" />
ng-change="vm.check()"
ng-disabled="state.disabled || form.disabled" />
</div>
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
{{ state.message }}
</p>
</div>
</div>

View File

@ -4,13 +4,16 @@
<div class="at-InputGroup at-InputSelect">
<input type="text" class="form-control at-Input at-InputSelect-input"
placeholder="{{ state.placeholder | uppercase }}"
ng-model="state.value" ng-disabled="state.disabled"
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-model="state.value"
ng-disabled="state.disabled || form.disabled"
ng-change="vm.check()" />
<select class="form-control at-InputSelect-select" ng-model="state.value"
ng-attr-tabindex="{{ tab || undefined }}"
ng-disabled="state.disabled">
<optgroup ng-repeat="group in state.data" label="{{::group.category | uppercase }}">
ng-disabled="state.disabled || form.disabled">
<optgroup ng-repeat="group in state.data"
label="{{::group.category | uppercase }}">
<option ng-repeat="item in group.data" value="{{ item.name }}">
{{ item.name }}
</option>
@ -18,5 +21,8 @@
</select>
<i class="fa" ng-class="{ 'fa-chevron-down': !open, 'fa-chevron-up': open }"></i>
</div>
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
{{ state.message }}
</p>
</div>
</div>

View File

@ -1,10 +1,17 @@
<div class="col-sm-{{::col}}">
<div class="form-group at-u-flat">
<at-input-label state="state"></at-input-label>
<input type="text" class="form-control at-Input" ng-model="state.value"
<input type="text" class="form-control at-Input"
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-model="state.value"
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state.placeholder || undefined }}"
ng-change="vm.check()" ng-disabled="state.disabled" />
ng-change="vm.check()"
ng-disabled="state.disabled || form.disabled" />
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
{{ state.message }}
</p>
</div>
</div>

View File

@ -2,10 +2,15 @@
<div class="form-group at-u-flat">
<at-input-label state="state"></at-input-label>
<textarea class="form-control at-Input" ng-model="state.value"
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state.placeholder || undefined }}"
ng-change="vm.check()" ng-disabled="state.disabled" />
ng-class="{ 'at-Input--rejected': state.rejected }"
ng-attr-maxlength="{{ state.options.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state.placeholder || undefined }}"
ng-change="vm.check()"
ng-disabled="state.disabled || form.disabled" />
</textarea>
<p ng-if="state.rejected && !state.isValid" class="at-InputMessage--rejected">
{{ state.message }}
</p>
</div>
</div>

View File

@ -1,16 +1,13 @@
function use (scope) {
scope.dismiss = this.dismiss;
}
function dismiss ($state) {
$state.go('^');
}
function AtPanelController ($state) {
let vm = this;
vm.dismiss = dismiss.bind(vm, $state);
vm.use = use;
vm.dismiss = () => {
$state.go('^');
};
vm.use = scope => {
scope.dismiss = this.dismiss;
};
}
AtPanelController.$inject = ['$state'];

View File

@ -1,49 +1,71 @@
let $resource;
function get() {
return $resource(this.path).get().$promise
.then(response => {
this.model.data = response;
});
}
function options () {
let actions = {
options: {
method: 'OPTIONS'
}
};
return $resource(this.path, null, actions).options().$promise
.then(response => {
this.model.options = response;
});
}
function getPostOptions (name) {
return this.model.options.actions.POST[name];
}
function normalizePath (resource) {
let version = '/api/v2/';
return `${version}${resource}/`;
}
function BaseModel (_$resource_) {
$resource = _$resource_;
function BaseModel ($http) {
return function extend (path) {
this.get = get;
this.options = options;
this.getPostOptions = getPostOptions;
this.normalizePath = normalizePath;
this.get = () => {
let request = {
method: 'GET',
url: this.path
};
return $http(request)
.then(response => {
this.model.get = response;
});
};
this.post = data => {
let request = {
method: 'POST',
url: this.path,
data,
};
return $http(request)
.then(response => {
this.model.post = response;
});
};
this.options = () => {
let request = {
method: 'OPTIONS',
url: this.path
};
return $http(request)
.then(response => {
this.model.options = response;
});
};
this.getOptions = (method, key) => {
if (!method) {
return this.model.options.data;
}
method = method.toUpperCase();
if (method && !key) {
return this.model.options.data.actions[method];
}
if (method && key) {
return this.model.options.data.actions[method][key];
}
return null;
};
this.normalizePath = resource => {
let version = '/api/v2/';
return `${version}${resource}/`;
};
this.model = {};
this.path = this.normalizePath(path);
};
}
BaseModel.$inject = ['$resource'];
BaseModel.$inject = ['$http'];
export default BaseModel;

View File

@ -1,7 +1,23 @@
function CredentialModel (BaseModel) {
function CredentialModel (BaseModel, CredentialTypeModel) {
BaseModel.call(this, 'credentials');
this.createFormSchema = (type, config) => {
let schema = Object.assign({}, this.getOptions(type));
if (config && config.omit) {
config.omit.forEach(key => {
delete schema[key];
});
}
for (let key in schema) {
schema[key].id = key;
}
return schema;
};
}
CredentialModel.$inject = ['BaseModel'];
CredentialModel.$inject = ['BaseModel', 'CredentialTypeModel'];
export default CredentialModel;

View File

@ -4,7 +4,7 @@ function CredentialTypeModel (BaseModel) {
this.categorizeByKind = () => {
let group = {};
this.model.data.results.forEach(result => {
this.model.get.data.results.forEach(result => {
group[result.kind] = group[result.kind] || [];
group[result.kind].push(result);
});
@ -16,7 +16,7 @@ function CredentialTypeModel (BaseModel) {
};
this.getTypeFromName = name => {
let type = this.model.data.results.filter(result => result.name === name);
let type = this.model.get.data.results.filter(result => result.name === name);
if (!type.length) {
return null;
@ -36,6 +36,10 @@ function CredentialTypeModel (BaseModel) {
return field;
});
};
this.getResults = () => {
return this.model.get.data.results;
};
}
CredentialTypeModel.$inject = ['BaseModel'];

View File

@ -2,15 +2,8 @@ import Base from './Base';
import Credential from './Credential';
import CredentialType from './CredentialType';
function config ($resourceProvider) {
$resourceProvider.defaults.stripTrailingSlashes = false;
}
config.$inject = ['$resourceProvider'];
angular
.module('at.lib.models', [])
.config(config)
.service('BaseModel', Base)
.service('CredentialModel', Credential)
.service('CredentialTypeModel', CredentialType);

View File

@ -136,7 +136,6 @@ var tower = angular.module('Tower', [
'AWDirectives',
'features',
'ngResource',
'at.lib.components',
'at.lib.models',
'at.lib.services',