UNPKG

fluid-form

Version:
345 lines (309 loc) 7.96 kB
'use strict'; var _ = require('lodash'); module.exports = fluidFormController; /*@ngInject*/ function fluidFormController($element, $scope, $parse, $timeout) { var ngModel = $element.controller('ngModel'); $scope.fieldChanged = fieldChanged; $scope.fieldSize = fieldSize; $scope.fieldFocus = fieldFocus; $scope.fieldBlur = fieldBlur; $scope.fieldIsFocused = fieldIsFocused; $scope.formIsValid = formIsValid; $scope.submitForm = submitForm; /* Dropdowns (TODO: Move to another directive) */ $scope.dropdown = { open: openDropDown, close: closeDropDown, toggle: toggleDropDown, isOpen: isOpenDropDown, select: selectDropDown, isSelected: isSelectedDropDown, focus: focusDropDown, blur: blurDropDown, keydown: keydownDropDown }; var activeDropDown = null; var isAllValid = false; var dropDownBlurCloseTimer = null; /* Cached getters and utility functions */ var getters = []; var getterLocals = { set: function (value) { return value !== undefined && value !== null && value !== ''; }, shown: function (name) { var field = _.findWhere(clauses, { isField: true, name: name }); if (!field) { throw new Error('Field "' + name + '" does not exist'); } return field.visible; }, valid: function () { return isAllValid; } }; /* Data */ var model = null; /* Schema */ var clauses = null; /* External bindings */ ngModel.$render = modelChanged; $scope.$watch('fluidForm', generateForm); $scope.$watch('validator', revalidateForm); /* Field with focus */ var focused = null; /* Debounced change handler */ var debounceChanged = []; var debounceTimer = null; function generateForm() { getters.length = 0; var schema = $scope.schema; clauses = !schema ? [] : schema.map(function (source) { var clause = _.clone(source); clause.classes = clause.classes || {}; clause.classes[clause.type] = true; /* Echo getters */ if (clause.type === 'echo') { clause.getter = getGetter(clause.label); } /* Dedupe and map condition expressions to getters */ clause.conditionGetters = clause.conditions.map(getGetter); /* Label */ clause.getLabel = getLabel; /* Visibility */ clause.canShow = canShow; clause.updateVisible = updateVisible; return clause; function canShow() { return _.every(clause.conditionGetters, 'value'); } function updateVisible() { clause.visible = clause.canShow(); clause.classes.visible = clause.visible; clause.classes.hidden = !clause.visible; } function getLabel() { var labels = $scope.labels; var label = labels ? labels[clause.name] : ''; if (label !== null && label !== undefined) { clause.label = String(label); } else if (!clause.label) { clause.label = clause.name; } return clause.label; } }); $scope.clauses = clauses; reloadModel(); } function modelChanged() { model = ngModel.$viewValue; reloadModel(); } /* Update data-bound visibility */ function updateVisible() { clauses.forEach(function (clause) { clause.updateVisible(); }); } function fieldSize(clause) { var value = clause.value || ''; return 1 + Math.max(3, value.length ? value.length : clause.label.length); } function fieldFocus(clause) { focused = clause; } function fieldBlur(clause) { if (fieldIsFocused(clause)) { focused = null; } revalidateForm(); } function fieldIsFocused(clause) { return focused === clause; } /* Dedupe and map expressions to getters with value cache */ function getGetter(expr) { var getter = _.findWhere(getters, { expr: expr }); if (!getter) { getter = { expr: expr, getter: $parse(expr), value: undefined }; getters.push(getter); } return getter; } /* Update cached values of bindings from echoes/conditions */ function updateBindings() { getters.forEach(function (getter) { getter.value = getter.getter(model, getterLocals); }); updateVisible(); } /* Reset the model */ function reloadModel() { if (!clauses) { return; } for (var i = 0; i < clauses.length; i++) { var clause = clauses[i]; var modelValue = model[clause.name]; if (modelValue !== undefined) { clause.value = modelValue; } } updateBindings(); _(clauses) .filter(_.property('value')) .each(fieldChanged) .value(); revalidateForm(); } function revalidateForm() { updateVisible(); if (!$scope.validator || !$scope.schema || !model) { isAllValid = false; return; } var errors = _.pairs($scope.validator({ model: model }) || {}); clauses.forEach(function (clause) { clause.classes.invalid = false; clause.isInvalid = false; clause.errorMessage = null; }); isAllValid = true; errors.forEach(function (error) { var name = error[0]; var msg = error[1]; var clause = _.findWhere(clauses, { name: name }); if (!clause) { throw new Error('Validator failed: key "' + name + '" is not defined'); } if (!clause.visible) { return; } var showAsInvalid = msg && msg.length; clause.classes.invalid = showAsInvalid; clause.isInvalid = showAsInvalid; clause.errorMessage = msg; isAllValid = false; }); updateVisible(); } /* Queue field for update and queue a delayed update */ function fieldChanged(clause) { debounceChanged.push(clause); if (debounceTimer) { $timeout.cancel(debounceTimer); } debounceTimer = $timeout(fieldsChanged, 100); } /* Update, after field value(s) changed */ function fieldsChanged(sendViewModel) { if (debounceTimer) { $timeout.cancel(debounceTimer); debounceTimer = null; } var changes = _.unique(debounceChanged); debounceChanged = []; var dirty = false; _(changes) .unique() .each(function (clause) { var value = clause.value; var isDirty = model[clause.name] !== value; if (isDirty) { dirty = true; model[clause.name] = value; } }) .value(); if (dirty || sendViewModel) { updateBindings(); revalidateForm(); } } function formIsValid() { return isAllValid; } function submitForm() { if (!isAllValid) { throw new Error('Cannot submit form, form is invalid'); } fieldsChanged(true); var result = _.pick(model, function (value, key) { return _.findWhere(clauses, { name: key }).visible; }); ngModel.$setViewValue(model); if ($scope.onSubmit) { $scope.onSubmit({ model: result }); } } /* Dropdown stuff, TODO move to new directive like ui-bootstrap except not shit */ function openDropDown(clause) { fieldFocus(clause); activeDropDown = clause; clause.classes.open = true; } function closeDropDown(clause) { if (activeDropDown !== clause) { return; } activeDropDown = null; clause.classes.open = false; fieldBlur(clause); } function toggleDropDown(clause) { if (isOpenDropDown(clause)) { closeDropDown(clause); } else { openDropDown(clause); } } function selectDropDown(clause, value, noClose) { clause.value = value; fieldChanged(clause); if (!noClose) { closeDropDown(clause); } } function isOpenDropDown(clause) { return activeDropDown === clause; } function isSelectedDropDown(clause, value) { return clause.value === value; } function focusDropDown(clause) { $timeout.cancel(dropDownBlurCloseTimer); fieldFocus(clause); } function blurDropDown(clause) { $timeout.cancel(dropDownBlurCloseTimer); dropDownBlurCloseTimer = $timeout(function () { closeDropDown(clause); }, 50); } function keydownDropDown(clause, event) { var idx = clause.choices.indexOf(clause.value); var len = clause.choices.length; switch (event.keyCode) { case 38: idx += (idx === -1) ? 1 : len - 1; break; case 40: idx += 1; break; case 32: case 27: case 13: case 10: $timeout(function () { closeDropDown(clause); }, 0); return; default: return; } idx = idx % len; selectDropDown(clause, clause.choices[idx], true); } }