From 3ab26b4a751d18d5543a311039dbd127cd3d2d56 Mon Sep 17 00:00:00 2001 From: Denis Dervisevic Date: Fri, 1 May 2015 20:05:40 +0200 Subject: [PATCH 01/10] Update README.md Changed some titles. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3532b643e..5ecd11c16 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Angular Schema Form Generate forms from JSON schemas using AngularJS! -Web Page / Twitter / Video +The Web Site / The Twitter / The Video -------- -[schemaform.io](http://schemaform.io) / [@SchemaFormIO](http://twitter.com/SchemaFormIO) / [The Movie](https://www.youtube.com/watch?v=duBFMipRq2o) +[schemaform.io](http://schemaform.io) / [@SchemaFormIO](http://twitter.com/SchemaFormIO) / [Movie](https://www.youtube.com/watch?v=duBFMipRq2o) Demo Time! ---------- From e5c618322eba14bf461ebb06fe26c53d9149707a Mon Sep 17 00:00:00 2001 From: Denis Dervisevic Date: Fri, 1 May 2015 20:06:25 +0200 Subject: [PATCH 02/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ecd11c16..d0a9dfd43 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Angular Schema Form Generate forms from JSON schemas using AngularJS! -The Web Site / The Twitter / The Video +The Web Site / The Twitter / The Movie -------- [schemaform.io](http://schemaform.io) / [@SchemaFormIO](http://twitter.com/SchemaFormIO) / [Movie](https://www.youtube.com/watch?v=duBFMipRq2o) From 3353ccaddaae1174b42e524b3b312762b6ae96d8 Mon Sep 17 00:00:00 2001 From: jbsaff Date: Tue, 28 Apr 2015 12:58:58 -0500 Subject: [PATCH 03/10] Extended schema-validate directive to handle cleaning the model when 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. --- docs/index.md | 9 ++ src/directives/schema-validate.js | 251 +++++++++++++++++++----------- src/services/unselect.js | 82 ++++++++++ 3 files changed, 248 insertions(+), 94 deletions(-) create mode 100644 src/services/unselect.js diff --git a/docs/index.md b/docs/index.md index e9a4abf15..08596f9c0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -824,6 +824,15 @@ function FormCtrl($scope) { Note that arrays inside arrays won't work with conditions. +### destroyStrategy +By default, when a field is removed from the DOM and the $destroy event is broadcast, the schema-validate directive +will update the model to set the field value to undefined. This can be overridden by setting the destroyStrategy +on a field to one of null, empty string (""), undefined, or "retain". Any other value will be ignored and the default +behavior will apply. The empty string option only applies to fields that have a type of string; using the empty string +with other field types will just be set to the default destroyStrategy. If you'd like to set the destroyStrategy for +an entire form, add it to the formDefaults in the [globalOptions](#global-options) + + Specific options and types diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index cfa335260..a1a735b3c 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -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; + }; - } - }; -}]); + } + }; + }]); diff --git a/src/services/unselect.js b/src/services/unselect.js new file mode 100644 index 000000000..8d4b96887 --- /dev/null +++ b/src/services/unselect.js @@ -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; + }; +}]); From 896ec3e771ed53979f399e884a13a395852ab84d Mon Sep 17 00:00:00 2001 From: jbsaff Date: Tue, 28 Apr 2015 13:04:51 -0500 Subject: [PATCH 04/10] Additional doc example. --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 08596f9c0..a4428ed64 100644 --- a/docs/index.md +++ b/docs/index.md @@ -635,6 +635,7 @@ General options most field types can handle: labelHtmlClass: "street" // CSS Class(es) to be added to the label of the field (or similar) copyValueTo: ["address.street"], // Copy values to these schema keys. condition: "person.age < 18" // Show or hide field depending on an angular expression + destroyStrategy: null // One of null, empty string, undefined, or 'retain'. Changes model on $destroy event. } ``` From e73bfe2887f772cbed289d2945c87ff6c142baf8 Mon Sep 17 00:00:00 2001 From: jbsaff Date: Tue, 28 Apr 2015 13:17:54 -0500 Subject: [PATCH 05/10] Part of another branch pull request. --- src/directives/decorators/bootstrap/checkboxes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directives/decorators/bootstrap/checkboxes.html b/src/directives/decorators/bootstrap/checkboxes.html index 3d65ca8c1..d420ea7c7 100644 --- a/src/directives/decorators/bootstrap/checkboxes.html +++ b/src/directives/decorators/bootstrap/checkboxes.html @@ -9,7 +9,7 @@ sf-changed="form" class="{{form.fieldHtmlClass}}" ng-model="titleMapValues[$index]" - schema-validate="form" + schema-vaidate="form" name="{{form.key.slice(-1)[0]}}"> From 76a562ce07ce1817367fd0ecc11f09066a675b00 Mon Sep 17 00:00:00 2001 From: jbsaff Date: Thu, 30 Apr 2015 10:16:29 -0500 Subject: [PATCH 06/10] Fixes from code review in #371. Reworked the default destroyStrategy to be picked up directly from globalOptions, instead of looking in formDefaults. Properly handles for the difference between the destroyStrategy declared as undefined and completely undeclared, both at the globalOptions level and the form field definition level. --- docs/index.md | 3 +- src/directives/schema-validate.js | 55 +++++++++++++++++++------------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/index.md b/docs/index.md index a4428ed64..7f870e0bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -193,6 +193,7 @@ attribute which should be placed along side `sf-schema`. | formDefaults | an object that will be used as a default for all form definitions | | validationMessage | an object or a function that will be used as default validation message for all fields. See [Validation Messages](#validation-messages) for details. | | setSchemaDefaults | boolean, set to false an no defaults from the schema will be set on the model. | +| destroyStrategy | the default strategy to use for cleaning the model when a form element is removed. see [destroyStrategy](#destroyStrategy) below | *formDefaults* is mostly useful for setting global [ngModelOptions](#ngmodeloptions) i.e. changing the entire form to validate on blur. @@ -831,7 +832,7 @@ will update the model to set the field value to undefined. This can be overridde on a field to one of null, empty string (""), undefined, or "retain". Any other value will be ignored and the default behavior will apply. The empty string option only applies to fields that have a type of string; using the empty string with other field types will just be set to the default destroyStrategy. If you'd like to set the destroyStrategy for -an entire form, add it to the formDefaults in the [globalOptions](#global-options) +an entire form, add it to the [globalOptions](#global-options) diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index a1a735b3c..75f958642 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -103,49 +103,60 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele }); - 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; + var DEFAULT_DESTROY_STRATEGY = getGlobalOptionsDestroyStrategy(); + + function getGlobalOptionsDestroyStrategy() { + var defaultStrategy = undefined; + if (scope.options && scope.options.hasOwnProperty('destroyStrategy')) { + var globalOptionsDestroyStrategy = scope.options.destroyStrategy; + var isValidFormDefaultDestroyStrategy = (globalOptionsDestroyStrategy === undefined || + globalOptionsDestroyStrategy === '' || + globalOptionsDestroyStrategy === null || + globalOptionsDestroyStrategy === 'retain'); + if (isValidFormDefaultDestroyStrategy) { + defaultStrategy = globalOptionsDestroyStrategy; + } + else { + console.warn('Unrecognized globalOptions destroyStrategy: %s \'%s\'. Used undefined instead.', + typeof globalOptionsDestroyStrategy, globalOptionsDestroyStrategy); + } } + return defaultStrategy; } // 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. + // Default behavior can be supplied as a globalOption, 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. + + // Either set in form definition, or as part of globalOptions. + var destroyStrategy = + !form.hasOwnProperty('destroyStrategy') ? DEFAULT_DESTROY_STRATEGY : form.destroyStrategy; 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); + console.warn('%s has defined unrecognized destroyStrategy: \'%s\'. Used default instead.', + attrs.name, 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.'); + console.warn('%s attempted to use empty string destroyStrategy on non-string form type. ' + + 'Used default instead.', attrs.name); destroyStrategy = DEFAULT_DESTROY_STRATEGY; } if (destroyStrategy === 'retain') { return; // Valid option to avoid destroying data in the model. } + console.log('result %s', destroyStrategy); destroyUsingStrategy(destroyStrategy); function destroyUsingStrategy(strategy) { - var strategyIsDefined = (strategy === null || strategy === '' || typeof strategy == undefined); + console.log('Destroy called with %s', strategy); + var strategyIsDefined = (strategy === null || strategy === '' || strategy === undefined); if (!strategyIsDefined){ strategy = DEFAULT_DESTROY_STRATEGY; } @@ -153,12 +164,14 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele } function getSchemaType() { + var sType; if (form.schema) { - schemaType = form.schema.type; + sType = form.schema.type; } else { - schemaType = null; + sType = null; } + return sType; } }); From 0d6f417f1039840789205d769e2eb36583a956ee Mon Sep 17 00:00:00 2001 From: jbsaff Date: Thu, 30 Apr 2015 10:18:57 -0500 Subject: [PATCH 07/10] Removed extra console logging. --- src/directives/schema-validate.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index 75f958642..2a9c0f761 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -150,12 +150,10 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele if (destroyStrategy === 'retain') { return; // Valid option to avoid destroying data in the model. } - console.log('result %s', destroyStrategy); destroyUsingStrategy(destroyStrategy); function destroyUsingStrategy(strategy) { - console.log('Destroy called with %s', strategy); var strategyIsDefined = (strategy === null || strategy === '' || strategy === undefined); if (!strategyIsDefined){ strategy = DEFAULT_DESTROY_STRATEGY; From 45683370a06204e39c73ee19229d8ced1c845394 Mon Sep 17 00:00:00 2001 From: jbsaff Date: Fri, 1 May 2015 15:35:44 -0500 Subject: [PATCH 08/10] Only clean model if the form's condition is no longer satisfied. This prevents the form from wiping itself when a $destroy event occurs that is not connected to the condition logic (such as if the FormController contains an element that wraps the form with an ng-if). --- src/directives/schema-validate.js | 77 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index 2a9c0f761..6f83b55eb 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -1,5 +1,5 @@ -angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', - function(sfValidator, sfSelect, sfUnselect) { +angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', '$parse', + function(sfValidator, sfSelect, sfUnselect, $parse) { return { restrict: 'A', @@ -128,48 +128,53 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele // Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition. scope.$on('$destroy', function() { var form = getForm(); + var conditionResult = $parse(form.condition); + console.log(conditionResult(scope)); - // Either set in form definition, or as part of globalOptions. - var destroyStrategy = - !form.hasOwnProperty('destroyStrategy') ? DEFAULT_DESTROY_STRATEGY : form.destroyStrategy; - var schemaType = getSchemaType(); + if (form.hasOwnProperty('condition') && !conditionResult(scope)) { // If condition is defined and not satisfied. - if (destroyStrategy && destroyStrategy !== 'retain' ) { - // Don't recognize the strategy, so give a warning. - console.warn('%s has defined unrecognized destroyStrategy: \'%s\'. Used default instead.', - attrs.name, 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('%s attempted to use empty string destroyStrategy on non-string form type. ' + - 'Used default instead.', attrs.name); - destroyStrategy = DEFAULT_DESTROY_STRATEGY; - } + // Either set in form definition, or as part of globalOptions. + var destroyStrategy = + !form.hasOwnProperty('destroyStrategy') ? DEFAULT_DESTROY_STRATEGY : form.destroyStrategy; + var schemaType = getSchemaType(); - if (destroyStrategy === 'retain') { - return; // Valid option to avoid destroying data in the model. - } - - destroyUsingStrategy(destroyStrategy); + if (destroyStrategy && destroyStrategy !== 'retain') { + // Don't recognize the strategy, so give a warning. + console.warn('%s has defined unrecognized destroyStrategy: \'%s\'. Used default instead.', + attrs.name, 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('%s attempted to use empty string destroyStrategy on non-string form type. ' + + 'Used default instead.', attrs.name); + destroyStrategy = DEFAULT_DESTROY_STRATEGY; + } - function destroyUsingStrategy(strategy) { - var strategyIsDefined = (strategy === null || strategy === '' || strategy === undefined); - if (!strategyIsDefined){ - strategy = DEFAULT_DESTROY_STRATEGY; + if (destroyStrategy === 'retain') { + return; // Valid option to avoid destroying data in the model. } - sfUnselect(scope.form.key, scope.model, strategy); - } - function getSchemaType() { - var sType; - if (form.schema) { - sType = form.schema.type; + destroyUsingStrategy(destroyStrategy); + + function destroyUsingStrategy(strategy) { + var strategyIsDefined = (strategy === null || strategy === '' || strategy === undefined); + if (!strategyIsDefined) { + strategy = DEFAULT_DESTROY_STRATEGY; + } + sfUnselect(scope.form.key, scope.model, strategy); } - else { - sType = null; + + function getSchemaType() { + var sType; + if (form.schema) { + sType = form.schema.type; + } + else { + sType = null; + } + return sType; } - return sType; } }); From d20b27318547f0f6e2af899e581a07e99739a91e Mon Sep 17 00:00:00 2001 From: jbsaff Date: Fri, 1 May 2015 15:57:04 -0500 Subject: [PATCH 09/10] Remove extraneous console log. --- src/directives/schema-validate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index d1994e56a..9aa2952e4 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -129,7 +129,6 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele scope.$on('$destroy', function() { var form = getForm(); var conditionResult = $parse(form.condition); - console.log(conditionResult(scope)); if (form.hasOwnProperty('condition') && !conditionResult(scope)) { // If condition is defined and not satisfied. From dcff62b5cdf71d16076cddef0b95523d2c20c74d Mon Sep 17 00:00:00 2001 From: jbsaff Date: Mon, 4 May 2015 15:50:00 -0500 Subject: [PATCH 10/10] sfSchema can communicate via sfRetainModel service to force the model to ignore the destroyStrategy when the $destroy event occurs on sfSchema. This will prevent the data from being cleaned up when the form is removed from the DOM. --- src/directives/schema-form.js | 7 ++++-- src/directives/schema-validate.js | 9 +++++--- src/services/retainModel.js | 37 +++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/services/retainModel.js diff --git a/src/directives/schema-form.js b/src/directives/schema-form.js index da2908601..aef650607 100644 --- a/src/directives/schema-form.js +++ b/src/directives/schema-form.js @@ -5,8 +5,8 @@ FIXME: real documentation angular.module('schemaForm') .directive('sfSchema', -['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', - function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath) { +['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfRetainModel', + function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfRetainModel) { var SNAKE_CASE_REGEXP = /[A-Z]/g; var snakeCase = function(name, separator) { @@ -161,6 +161,9 @@ angular.module('schemaForm') } }); + scope.$on('$destroy', function() { + sfRetainModel.setFlag(true); + }); } }; } diff --git a/src/directives/schema-validate.js b/src/directives/schema-validate.js index 9aa2952e4..50ee72a30 100644 --- a/src/directives/schema-validate.js +++ b/src/directives/schema-validate.js @@ -1,5 +1,5 @@ -angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', '$parse', - function(sfValidator, sfSelect, sfUnselect, $parse) { +angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', '$parse', 'sfRetainModel', + function(sfValidator, sfSelect, sfUnselect, $parse, sfRetainModel) { return { restrict: 'A', @@ -127,10 +127,13 @@ angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSele // Clean up the model when the corresponding form field is $destroy-ed. // Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition. scope.$on('$destroy', function() { + var form = getForm(); var conditionResult = $parse(form.condition); + var formModelNotRetained = !sfRetainModel.getFlag(); - if (form.hasOwnProperty('condition') && !conditionResult(scope)) { // If condition is defined and not satisfied. + // If condition is defined and not satisfied and the sfSchema model should not be retained. + if (form.hasOwnProperty('condition') && !conditionResult(scope) && formModelNotRetained) { // Either set in form definition, or as part of globalOptions. var destroyStrategy = diff --git a/src/services/retainModel.js b/src/services/retainModel.js new file mode 100644 index 000000000..e500d3d3d --- /dev/null +++ b/src/services/retainModel.js @@ -0,0 +1,37 @@ +angular.module('schemaForm').factory('sfRetainModel', function() { + + var data = {retainModelFlag: false }; + + return { + /** + * @description + * Utility service to indicate if the sfSchema model should be retained. + * Set to true to prevent an operation that would have destroyed the model + * from doing so (such as wrapping the form in an ng-if). + * + * ex. + * var foo = sfRetainModel.getFlag(); + * + * @returns {boolean} returns the current value of the retainModelFlag. + */ + getFlag: function () { + return data.retainModelFlag; + }, + + /** + * @description + * Set the value of the retainModelFlag. + * True prevents cleaning the data in the model, while false follows the configured destroyStrategy. + * + * ex. + * var bar = sfRetainModel.setFlag(true); + * + * @param {boolean} value The boolean value to set as the retainModelFlag + * @returns {boolean} returns the value of the retainModelFlag after toggling. + */ + setFlag: function(value) { + data.retainModelFlag = value; + return data.retainModelFlag; + } + } +}); \ No newline at end of file