Add form-updating input validation for components

This commit is contained in:
gconsidine 2017-05-11 15:22:17 -04:00
parent 725fd15519
commit 29a325d52f
13 changed files with 191 additions and 88 deletions

View File

@ -2,9 +2,11 @@ function AddCredentialsController (credentialType) {
let vm = this || {};
vm.name = {
state: {
required: true
},
label: {
text: 'Name',
required: true,
popover: {
text: 'a, b, c'
}
@ -22,9 +24,11 @@ function AddCredentialsController (credentialType) {
};
vm.kind = {
state: {
required: true,
},
label: {
text: 'Type',
required: true,
popover: {
text: 'x, y, z'
}

View File

@ -8,13 +8,13 @@
<at-panel-body>
<at-form>
<at-input-text col="4" config="vm.name"></at-input-text>
<at-input-text col="4" config="vm.description"></at-input-text>
<at-input-select col="4" config="vm.kind"></at-input-select>
<at-input-text tab="1" col="4" config="vm.name"></at-input-text>
<at-input-text tab="2" col="4" config="vm.description"></at-input-text>
<at-input-select tab="3" col="4" config="vm.kind"></at-input-select>
<at-action-group col="12" pos="right">
<at-action config="vm.cancel"></at-action>
<at-action config="vm.save"></at-action>
<at-action tab="4" config="vm.cancel"></at-action>
<at-action tab="5" config="vm.save"></at-action>
</at-action-group>
</at-form>
</at-panel-body>

View File

@ -1,30 +1,33 @@
let $state;
function applyCancelProperties (scope) {
scope.text = scope.config.text || 'CANCEL';
scope.fill = 'Hollow';
scope.color = 'white';
scope.disabled = false;
scope.action = () => $state.go('^');
}
function applySaveProperties (scope) {
scope.text = 'SAVE';
scope.fill = '';
scope.color = 'green';
scope.disabled = true;
}
function link (scope, el, attrs, form) {
form.use('action', scope, el);
scope.config.state = scope.config.state || {};
let state = scope.config.state;
scope.form = form.use('action', state);
switch(scope.config.type) {
case 'cancel':
applyCancelProperties(scope);
setCancelDefaults(scope);
break;
case 'save':
applySaveProperties(scope);
setSaveDefaults(scope);
break;
default:
break;
}
function setCancelDefaults (scope) {
scope.text = 'CANCEL';
scope.fill = 'Hollow';
scope.color = 'white';
scope.action = () => $state.go('^');
}
function setSaveDefaults (scope) {
scope.text = 'SAVE';
scope.fill = '';
scope.color = 'green';
}
}

View File

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

View File

@ -1,56 +1,70 @@
function use (type, componentScope, componentElement) {
function use (type, component, el) {
let vm = this;
let component;
let state;
switch (type) {
case 'input':
component = vm.trackInput(componentElement);
state = vm.trackInput(component, el);
break;
case 'action':
component = vm.trackAction(componentElement);
state = vm.trackAction(component, el);
break;
default:
throw new Error('An at-form cannot use component type:', type);
}
componentScope.meta = component;
return state;
}
function trackInput (componentElement) {
function trackInput (component, el) {
let vm = this;
let input = {
el: componentElement,
tabindex: vm.inputs.length + 1
let form = {
state: vm.state,
disabled: false
};
if (vm.inputs.length === 0) {
input.autofocus = true;
componentElement.find('input').focus();
}
vm.inputs.push(component)
vm.inputs.push(input)
return input;
return form;
}
function trackAction (componentElement) {
function trackAction (component) {
let vm = this;
let action = {
el: componentElement
let form = {
state: vm.state,
disabled: false
};
vm.actions.push(action);
vm.actions.push(component);
return action;
return form;
}
function update () {
function validate () {
let vm = this;
vm.inputs.forEach(input => console.log(input));
let isValid = true;
vm.inputs.forEach(input => {
if (!input.isValid) {
isValid = false;
}
});
return isValid;
}
function check () {
let vm = this;
let isValid = vm.validate();
if (isValid !== vm.state.isValid) {
vm.state.isValid = isValid;
}
}
function remove (id) {
@ -62,13 +76,18 @@ function remove (id) {
function AtFormController () {
let vm = this;
vm.state = {
isValid: false
};
vm.inputs = [];
vm.actions = [];
vm.use = use;
vm.trackInput = trackInput;
vm.trackAction = trackAction;
vm.update = update;
vm.validate = validate;
vm.check = check;
vm.remove = remove;
}

View File

@ -1,5 +1,5 @@
<form>
<div class="row">
<ng-transclude></ng-transclude>
</div>
<div class="row">
<ng-transclude></ng-transclude>
</div>
</form>

View File

@ -1,5 +1,5 @@
<label class="at-InputLabel at-u-flat">
<span ng-if="config.required" class="pull-left at-InputLabel-required">*</span>
<span class="pull-left">{{ config.text }}</span>
<at-popover class="pull-left" config="config.popover"></at-popover>
<span ng-if="config.state.required" class="pull-left at-InputLabel-required">*</span>
<span class="pull-left">{{::config.label.text}}</span>
<at-popover class="pull-left" config="config.label.popover"></at-popover>
</label>

View File

@ -2,16 +2,24 @@ let eventService;
let pathService;
function link (scope, el, attrs, form) {
form.use('input', scope, el); // avoid passing scope? assign to scope.meta instead or reference form properties in view
scope.config.state = scope.config.state || {};
let input = el.find('input')[0];
let select = el.find('select')[0];
let state = scope.config.state;
setDefaults();
scope.form = form.use('input', state);
let listeners = eventService.addListeners(scope, [
[input, 'focus', () => select.focus()],
[select, 'mousedown', () => scope.open = !scope.open],
[input, 'focus', () => select.focus],
[select, 'mousedown', () => scope.$apply(scope.open = !scope.open)],
[select, 'focus', () => input.classList.add('at-Input--focus')],
[select, 'change', () => scope.open = false],
[select, 'change', () => {
scope.open = false;
check();
}],
[select, 'blur', () => {
input.classList.remove('at-Input--focus');
scope.open = scope.open && false;
@ -20,13 +28,38 @@ function link (scope, el, attrs, form) {
scope.$on('$destroy', () => eventService.remove(listeners));
/*
* Should notify form on:
* - valid (required, passes validation) state change
*
* Should get from form:
* - display as disabled
*/
function setDefaults () {
if (scope.tab === 1) {
select.focus();
}
state.isValid = state.isValid || false;
state.validate = state.validate ? validate.bind(null, state.validate) : validate;
state.check = state.check || check;
state.message = state.message || '';
state.required = state.required || false;
}
function validate (fn) {
let isValid = true;
if (state.required && !state.value) {
isValid = false;
} else if (fn && !fn(scope.config.input)) {
isValid = false;
}
return isValid;
}
function check () {
let isValid = state.validate();
if (isValid !== state.isValid) {
state.isValid = isValid;
form.check();
}
}
}
function atInputSelect (_eventService_, _pathService_) {
@ -42,7 +75,8 @@ function atInputSelect (_eventService_, _pathService_) {
link,
scope: {
config: '=',
col: '@'
col: '@',
tab: '@'
}
};
}

View File

@ -1,14 +1,17 @@
<div class="col-sm-{{ col }}">
<div class="form-group at-u-flat">
<at-input-label config="config.label"></at-input-label>
<at-input-label config="config"></at-input-label>
<div class="at-InputGroup at-InputSelect">
<input type="text" class="form-control at-Input at-InputSelect-input"
ng-attr-autofocus="{{ meta.autofocus || undefined }}"
placeholder="{{ config.placeholder | uppercase }}" ng-model="config.input" />
<select class="form-control at-InputSelect-select" ng-model="config.input"
tabindex="{{ meta.tabindex }}">
<optgroup ng-repeat="group in config.data" label="{{ group.category | uppercase }}">
placeholder="{{ config.placeholder | uppercase }}"
ng-model="config.state.value" ng-disabled="form.disabled"
ng-change="config.state.check" />
<select class="form-control at-InputSelect-select" ng-model="config.state.value"
tabindex="{{::tab}}" ng-attr-autofocus="{{ tab == 1 || undefined }}"
ng-disabled="form.disabled">
<optgroup ng-repeat="group in config.data" label="{{::group.category | uppercase }}">
<option ng-repeat="item in group.data" value="{{ item.name }}">
{{ item.name }}
</option>

View File

@ -1,5 +1,44 @@
function link (scope, el, attrs, form) {
form.use('input', scope, el);
scope.config.state = scope.config.state || {};
let state = scope.config.state;
let input = el.find('input')[0];
setDefaults();
scope.form = form.use('input', state, input);
function setDefaults () {
if (scope.tab === '1') {
input.focus();
}
state.isValid = state.isValid || false;
state.validate = state.validate ? validate.bind(null, state.validate) : validate;
state.check = state.check || check;
state.message = state.message || '';
state.required = state.required || false;
}
function validate (fn) {
let isValid = true;
if (state.required && !state.value) {
isValid = false;
} else if (fn && !fn(scope.config.input)) {
isValid = false;
}
return isValid;
}
function check () {
let isValid = state.validate();
if (isValid !== state.isValid) {
state.isValid = isValid;
form.check();
}
}
}
function atInputText (pathService) {
@ -12,7 +51,8 @@ function atInputText (pathService) {
link,
scope: {
config: '=',
col: '@'
col: '@',
tab: '@'
}
};
}

View File

@ -1,8 +1,9 @@
<div class="col-sm-{{ col }}">
<div class="col-sm-{{::col}}">
<div class="form-group at-u-flat">
<at-input-label config="config.label"></at-input-label>
<input type="text" class="form-control at-Input" ng-model="config.input"
ng-attr-autofocus="{{ meta.autofocus || undefined }}" tabindex="{{ meta.tabindex }}"
placeholder="{{ config.placeholder }}" />
<at-input-label config="config"></at-input-label>
<input type="text" class="form-control at-Input" ng-model="config.state.value"
ng-attr-autofocus="{{tab == 1 || undefined }}"
tabindex="{{::tab}}" placeholder="{{::config.placeholder}}"
ng-change="config.state.check()" ng-disabled="form.disabled" />
</div>
</div>

View File

@ -6,6 +6,6 @@
<div class="at-Popover-arrow">
<i class="fa fa-caret-left fa-2x"></i>
</div>
<div class="at-Popover-content">{{ config.text }}</div>
<div class="at-Popover-content">{{::config.text}}</div>
</div>
</div>

View File

@ -8,11 +8,9 @@ function addListeners (scope, list) {
return listeners;
}
function addListener (scope, el, name, fn, type) {
type = type || '$apply';
function addListener (scope, el, name, fn) {
let listener = {
fn: () => scope[type](fn),
fn,
name,
el
};