-
Notifications
You must be signed in to change notification settings - Fork 641
Feature: clean model when a field is removed from view, such as when a condition is no longer satisfied. #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
3ab26b4
e5c6183
3353cca
896ec3e
e73bfe2
76a562c
0d6f417
4568337
4d72f3d
d20b273
dcff62b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…a form field element triggers the $destroy. Uses a new service, based on Select, to traverse the model and update it to the value chosen as part of the configured destroyStrategy. This destroyStrategy can be configured at the field, or as part of the forms global options. If both are defined, the field-level strategy will override.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,110 +1,173 @@ | ||
| angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', function(sfValidator, sfSelect) { | ||
| return { | ||
| restrict: 'A', | ||
| scope: false, | ||
| // We want the link function to be *after* the input directives link function so we get access | ||
| // the parsed value, ex. a number instead of a string | ||
| priority: 500, | ||
| require: 'ngModel', | ||
| link: function(scope, element, attrs, ngModel) { | ||
|
|
||
|
|
||
| // We need the ngModelController on several places, | ||
| // most notably for errors. | ||
| // So we emit it up to the decorator directive so it can put it on scope. | ||
| scope.$emit('schemaFormPropagateNgModelController', ngModel); | ||
|
|
||
| var error = null; | ||
|
|
||
| var getForm = function() { | ||
| if (!form) { | ||
| form = scope.$eval(attrs.schemaValidate); | ||
| } | ||
| return form; | ||
| }; | ||
| var form = getForm(); | ||
| if (form.copyValueTo) { | ||
| ngModel.$viewChangeListeners.push(function() { | ||
| var paths = form.copyValueTo; | ||
| angular.forEach(paths, function(path) { | ||
| sfSelect(path, scope.model, ngModel.$modelValue); | ||
| angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', | ||
| function(sfValidator, sfSelect, sfUnselect) { | ||
|
|
||
| return { | ||
| restrict: 'A', | ||
| scope: false, | ||
| // We want the link function to be *after* the input directives link function so we get access | ||
| // the parsed value, ex. a number instead of a string | ||
| priority: 500, | ||
| require: 'ngModel', | ||
| link: function(scope, element, attrs, ngModel) { | ||
|
|
||
| // We need the ngModelController on several places, | ||
| // most notably for errors. | ||
| // So we emit it up to the decorator directive so it can put it on scope. | ||
| scope.$emit('schemaFormPropagateNgModelController', ngModel); | ||
|
|
||
| var error = null; | ||
|
|
||
| var getForm = function() { | ||
| if (!form) { | ||
| form = scope.$eval(attrs.schemaValidate); | ||
| } | ||
| return form; | ||
| }; | ||
| var form = getForm(); | ||
| if (form.copyValueTo) { | ||
| ngModel.$viewChangeListeners.push(function() { | ||
| var paths = form.copyValueTo; | ||
| angular.forEach(paths, function(path) { | ||
| sfSelect(path, scope.model, ngModel.$modelValue); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Validate against the schema. | ||
| // Validate against the schema. | ||
|
|
||
| var validate = function(viewValue) { | ||
| form = getForm(); | ||
| //Still might be undefined | ||
| if (!form) { | ||
| return viewValue; | ||
| } | ||
| var validate = function(viewValue) { | ||
| form = getForm(); | ||
| //Still might be undefined | ||
| if (!form) { | ||
| return viewValue; | ||
| } | ||
|
|
||
| // Omit TV4 validation | ||
| if (scope.options && scope.options.tv4Validation === false) { | ||
| return viewValue; | ||
| } | ||
| // Omit TV4 validation | ||
| if (scope.options && scope.options.tv4Validation === false) { | ||
| return viewValue; | ||
| } | ||
|
|
||
| var result = sfValidator.validate(form, viewValue); | ||
| // Since we might have different tv4 errors we must clear all | ||
| // errors that start with tv4- | ||
| Object.keys(ngModel.$error) | ||
| var result = sfValidator.validate(form, viewValue); | ||
| // Since we might have different tv4 errors we must clear all | ||
| // errors that start with tv4- | ||
| Object.keys(ngModel.$error) | ||
| .filter(function(k) { return k.indexOf('tv4-') === 0; }) | ||
| .forEach(function(k) { ngModel.$setValidity(k, true); }); | ||
|
|
||
| if (!result.valid) { | ||
| // it is invalid, return undefined (no model update) | ||
| ngModel.$setValidity('tv4-' + result.error.code, false); | ||
| error = result.error; | ||
| return undefined; | ||
| if (!result.valid) { | ||
| // it is invalid, return undefined (no model update) | ||
| ngModel.$setValidity('tv4-' + result.error.code, false); | ||
| error = result.error; | ||
| return undefined; | ||
| } | ||
| return viewValue; | ||
| }; | ||
|
|
||
| // Custom validators, parsers, formatters etc | ||
| if (typeof form.ngModel === 'function') { | ||
| form.ngModel(ngModel); | ||
| } | ||
| return viewValue; | ||
| }; | ||
|
|
||
| // Custom validators, parsers, formatters etc | ||
| if (typeof form.ngModel === 'function') { | ||
| form.ngModel(ngModel); | ||
| } | ||
| ['$parsers', '$viewChangeListeners', '$formatters'].forEach(function(attr) { | ||
| if (form[attr] && ngModel[attr]) { | ||
| form[attr].forEach(function(fn) { | ||
| ngModel[attr].push(fn); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| ['$parsers', '$viewChangeListeners', '$formatters'].forEach(function(attr) { | ||
| if (form[attr] && ngModel[attr]) { | ||
| form[attr].forEach(function(fn) { | ||
| ngModel[attr].push(fn); | ||
| }); | ||
| } | ||
| }); | ||
| ['$validators', '$asyncValidators'].forEach(function(attr) { | ||
| // Check if our version of angular has i, i.e. 1.3+ | ||
| if (form[attr] && ngModel[attr]) { | ||
| angular.forEach(form[attr], function(fn, name) { | ||
| ngModel[attr][name] = fn; | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| ['$validators', '$asyncValidators'].forEach(function(attr) { | ||
| // Check if our version of angular has i, i.e. 1.3+ | ||
| if (form[attr] && ngModel[attr]) { | ||
| angular.forEach(form[attr], function(fn, name) { | ||
| ngModel[attr][name] = fn; | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| // Get in last of the parses so the parsed value has the correct type. | ||
| // We don't use $validators since we like to set different errors depeding tv4 error codes | ||
| ngModel.$parsers.push(validate); | ||
|
|
||
| // Listen to an event so we can validate the input on request | ||
| scope.$on('schemaFormValidate', function() { | ||
| if (ngModel.$setDirty) { | ||
| // Angular 1.3+ | ||
| ngModel.$setDirty(); | ||
| validate(ngModel.$modelValue); | ||
| } else { | ||
| // Angular 1.2 | ||
| ngModel.$setViewValue(ngModel.$viewValue); | ||
| // Get in last of the parses so the parsed value has the correct type. | ||
| // We don't use $validators since we like to set different errors depeding tv4 error codes | ||
| ngModel.$parsers.push(validate); | ||
|
|
||
| // Listen to an event so we can validate the input on request | ||
| scope.$on('schemaFormValidate', function() { | ||
| if (ngModel.$setDirty) { | ||
| // Angular 1.3+ | ||
| ngModel.$setDirty(); | ||
| validate(ngModel.$modelValue); | ||
| } else { | ||
| // Angular 1.2 | ||
| ngModel.$setViewValue(ngModel.$viewValue); | ||
| } | ||
|
|
||
| }); | ||
|
|
||
|
|
||
| var DEFAULT_DESTROY_STRATEGY; | ||
| if (scope.options && scope.options.formDefaults) { | ||
| var formDefaultDestroyStrategy = scope.options.formDefaults.destroyStrategy; | ||
| var isValidFormDefaultDestroyStrategy = (formDefaultDestroyStrategy === undefined || | ||
| formDefaultDestroyStrategy === '' || | ||
| formDefaultDestroyStrategy === null || | ||
| formDefaultDestroyStrategy === 'retain'); | ||
| if (isValidFormDefaultDestroyStrategy) { | ||
| DEFAULT_DESTROY_STRATEGY = formDefaultDestroyStrategy; | ||
| } | ||
| else { | ||
| console.warn('Unrecognized formDefaults.destroyStrategy: \'%s\'. Used undefined instead.', | ||
| formDefaultDestroyStrategy); | ||
| DEFAULT_DESTROY_STRATEGY = undefined; | ||
| } | ||
| } | ||
|
|
||
| }); | ||
| // Clean up the model when the corresponding form field is $destroy-ed. | ||
| // Default behavior can be supplied as a formDefault, and behavior can be overridden in the form definition. | ||
| scope.$on('$destroy', function() { | ||
| var form = getForm(); | ||
| var destroyStrategy = form.destroyStrategy; // Either set in form definition, or as part of formDefaults. | ||
| var schemaType = getSchemaType(); | ||
|
|
||
| if (destroyStrategy && destroyStrategy !== 'retain' ) { | ||
| // Don't recognize the strategy, so give a warning. | ||
| console.warn('Unrecognized destroyStrategy: \'%s\'. Used default instead.', destroyStrategy); | ||
| destroyStrategy = DEFAULT_DESTROY_STRATEGY; | ||
| } | ||
| else if (schemaType !== 'string' && destroyStrategy === '') { | ||
| // Only 'string' type fields can have an empty string value as a valid option. | ||
| console.warn('Attempted to use empty string destroyStrategy on non-string form type. Used default instead.'); | ||
| destroyStrategy = DEFAULT_DESTROY_STRATEGY; | ||
| } | ||
|
|
||
| if (destroyStrategy === 'retain') { | ||
| return; // Valid option to avoid destroying data in the model. | ||
| } | ||
|
|
||
| destroyUsingStrategy(destroyStrategy); | ||
|
|
||
| function destroyUsingStrategy(strategy) { | ||
| var strategyIsDefined = (strategy === null || strategy === '' || typeof strategy == undefined); | ||
|
||
| if (!strategyIsDefined){ | ||
| strategy = DEFAULT_DESTROY_STRATEGY; | ||
| } | ||
| sfUnselect(scope.form.key, scope.model, strategy); | ||
| } | ||
|
|
||
| function getSchemaType() { | ||
|
||
| if (form.schema) { | ||
| schemaType = form.schema.type; | ||
| } | ||
| else { | ||
| schemaType = null; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
|
|
||
|
|
||
| scope.schemaError = function() { | ||
| return error; | ||
| }; | ||
| scope.schemaError = function() { | ||
| return error; | ||
| }; | ||
|
|
||
| } | ||
| }; | ||
| }]); | ||
| } | ||
| }; | ||
| }]); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| angular.module('schemaForm').factory('sfUnselect', ['sfPath', function(sfPath) { | ||
| var numRe = /^\d+$/; | ||
|
|
||
| /** | ||
| * @description | ||
| * Utility method to clear deep properties without | ||
| * throwing errors when things are not defined. | ||
| * DOES NOT create objects when they are missing. | ||
| * | ||
| * Based on sfSelect. | ||
| * | ||
| * ex. | ||
| * var foo = Unselect('address.contact.name',obj, null) | ||
| * var bar = Unselect('address.contact.name',obj, undefined) | ||
| * Unselect('address.contact.name',obj,'') | ||
| * | ||
| * @param {string} projection A dot path to the property you want to set | ||
| * @param {object} obj (optional) The object to project on, defaults to 'this' | ||
| * @param {Any} unselectValue The value to set; if parts of the path of | ||
| * the projection is missing empty objects will NOT be created. | ||
| * @returns {Any|undefined} returns the value at the end of the projection path | ||
| * or undefined if there is none. | ||
| */ | ||
| return function(projection, obj, unselectValue) { | ||
| if (!obj) { | ||
| obj = this; | ||
| } | ||
| //Support [] array syntax | ||
| var parts = typeof projection === 'string' ? sfPath.parse(projection) : projection; | ||
| //console.log(parts); | ||
|
|
||
| if (parts.length === 1) { | ||
| //Special case, just setting one variable | ||
|
|
||
| //console.log('Only 1 variable in parts'); | ||
| obj[parts[0]] = unselectValue; | ||
| return obj; | ||
| } | ||
|
|
||
| if (typeof obj[parts[0]] === 'undefined') { | ||
| // If top-level part isn't defined. | ||
| var isArray = numRe.test(parts[1]); | ||
| if (isArray) { | ||
| //console.info('Expected array as top-level part, but is already undefined. Returning.'); | ||
| return undefined; | ||
| } | ||
| else if (parts.length > 2) { | ||
| obj[parts[0]] = {}; | ||
| } | ||
| } | ||
|
|
||
| var value = obj[parts[0]]; | ||
| for (var i = 1; i < parts.length; i++) { | ||
| // Special case: We allow JSON Form syntax for arrays using empty brackets | ||
| // These will of course not work here so we exit if they are found. | ||
| if (parts[i] === '') { | ||
| return undefined; | ||
| } | ||
|
|
||
| var tmp = value[parts[i]]; | ||
| if (i === parts.length - 1 ) { | ||
| //End of projection; setting the value | ||
|
|
||
| //console.log('Value set using destroyStrategy.'); | ||
| value[parts[i]] = unselectValue; | ||
| return unselectValue; | ||
| } else { | ||
| // Make sure to NOT create new objects on the way if they are not there. | ||
| // We need to look ahead to check if array is appropriate. | ||
| // Believe that if an array/object isn't present/defined, we can return. | ||
|
|
||
| //console.log('Processing part %s', parts[i]); | ||
| if (typeof tmp === 'undefined' || tmp === null) { | ||
| //console.log('Part is undefined; returning.'); | ||
| return undefined; | ||
| } | ||
| value = tmp; | ||
| } | ||
| } | ||
| return value; | ||
| }; | ||
| }]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formDefaultsshould not be checked directly, it's already copied to the form object, see https://github.com/Textalk/angular-schema-form/blob/development/src/services/schema-form.js#L70But in this case with
nullas a valid option distinct fromundefinedit might be better to move this to a global option. i.e. instead of checkingscope.option.formDefaults.destroyStrategyjust checkscope.option.destroyStrategyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this again, I think I might have overlooked the implications of having the formDefaults copied to the form object. (It makes sense now, so I may have just had the wrong idea when I started trying to use it). It seemed like a convenient way to set the default for the whole form if someone wanted to use an alternative to undefined, but a standard global option accomplishes that. I'll make a change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! :)