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

View File

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

View File

@@ -1,30 +1,33 @@
let $state; 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) { 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) { switch(scope.config.type) {
case 'cancel': case 'cancel':
applyCancelProperties(scope); setCancelDefaults(scope);
break; break;
case 'save': case 'save':
applySaveProperties(scope); setSaveDefaults(scope);
break; 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" <button class="btn at-Button{{ fill }}--{{ color }}"
ng-class="{ 'at-Button--disabled': disabled }" ng-click="action()"> ng-disabled="config.type !== 'cancel' && !form.state.isValid"
{{ text }} ng-class="{ 'at-Button--disabled': form.disabled }" ng-click="action()">
{{::text}}
</button> </button>

View File

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

View File

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

View File

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

View File

@@ -2,16 +2,24 @@ let eventService;
let pathService; let pathService;
function link (scope, el, attrs, form) { 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 input = el.find('input')[0];
let select = el.find('select')[0]; let select = el.find('select')[0];
let state = scope.config.state;
setDefaults();
scope.form = form.use('input', state);
let listeners = eventService.addListeners(scope, [ let listeners = eventService.addListeners(scope, [
[input, 'focus', () => select.focus()], [input, 'focus', () => select.focus],
[select, 'mousedown', () => scope.open = !scope.open], [select, 'mousedown', () => scope.$apply(scope.open = !scope.open)],
[select, 'focus', () => input.classList.add('at-Input--focus')], [select, 'focus', () => input.classList.add('at-Input--focus')],
[select, 'change', () => scope.open = false], [select, 'change', () => {
scope.open = false;
check();
}],
[select, 'blur', () => { [select, 'blur', () => {
input.classList.remove('at-Input--focus'); input.classList.remove('at-Input--focus');
scope.open = scope.open && false; scope.open = scope.open && false;
@@ -20,13 +28,38 @@ function link (scope, el, attrs, form) {
scope.$on('$destroy', () => eventService.remove(listeners)); scope.$on('$destroy', () => eventService.remove(listeners));
/* function setDefaults () {
* Should notify form on: if (scope.tab === 1) {
* - valid (required, passes validation) state change select.focus();
* }
* Should get from form:
* - display as disabled 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_) { function atInputSelect (_eventService_, _pathService_) {
@@ -42,7 +75,8 @@ function atInputSelect (_eventService_, _pathService_) {
link, link,
scope: { scope: {
config: '=', config: '=',
col: '@' col: '@',
tab: '@'
} }
}; };
} }

View File

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

View File

@@ -1,5 +1,44 @@
function link (scope, el, attrs, form) { 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) { function atInputText (pathService) {
@@ -12,7 +51,8 @@ function atInputText (pathService) {
link, link,
scope: { scope: {
config: '=', 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"> <div class="form-group at-u-flat">
<at-input-label config="config.label"></at-input-label> <at-input-label config="config"></at-input-label>
<input type="text" class="form-control at-Input" ng-model="config.input" <input type="text" class="form-control at-Input" ng-model="config.state.value"
ng-attr-autofocus="{{ meta.autofocus || undefined }}" tabindex="{{ meta.tabindex }}" ng-attr-autofocus="{{tab == 1 || undefined }}"
placeholder="{{ config.placeholder }}" /> tabindex="{{::tab}}" placeholder="{{::config.placeholder}}"
ng-change="config.state.check()" ng-disabled="form.disabled" />
</div> </div>
</div> </div>

View File

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

View File

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