Skip to content

Commit 30069ff

Browse files
committed
Merge branch 'feature/cleanModelUsingDestroyStrategy' of https://github.com/jbsaff/angular-schema-form into jbsaff-feature/cleanModelUsingDestroyStrategy
2 parents 490786a + dcff62b commit 30069ff

File tree

7 files changed

+311
-98
lines changed

7 files changed

+311
-98
lines changed

docs/index.md

+11
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ attribute which should be placed along side `sf-schema`.
193193
| formDefaults | an object that will be used as a default for all form definitions |
194194
| validationMessage | an object or a function that will be used as default validation message for all fields. See [Validation Messages](#validation-messages) for details. |
195195
| setSchemaDefaults | boolean, set to false an no defaults from the schema will be set on the model. |
196+
| destroyStrategy | the default strategy to use for cleaning the model when a form element is removed. see [destroyStrategy](#destroyStrategy) below |
196197
197198
*formDefaults* is mostly useful for setting global [ngModelOptions](#ngmodeloptions)
198199
i.e. changing the entire form to validate on blur.
@@ -635,6 +636,7 @@ General options most field types can handle:
635636
labelHtmlClass: "street" // CSS Class(es) to be added to the label of the field (or similar)
636637
copyValueTo: ["address.street"], // Copy values to these schema keys.
637638
condition: "person.age < 18" // Show or hide field depending on an angular expression
639+
destroyStrategy: null // One of null, empty string, undefined, or 'retain'. Changes model on $destroy event.
638640
}
639641
```
640642
@@ -824,6 +826,15 @@ function FormCtrl($scope) {
824826
Note that arrays inside arrays won't work with conditions.
825827
826828
829+
### destroyStrategy
830+
By default, when a field is removed from the DOM and the $destroy event is broadcast, the schema-validate directive
831+
will update the model to set the field value to undefined. This can be overridden by setting the destroyStrategy
832+
on a field to one of null, empty string (""), undefined, or "retain". Any other value will be ignored and the default
833+
behavior will apply. The empty string option only applies to fields that have a type of string; using the empty string
834+
with other field types will just be set to the default destroyStrategy. If you'd like to set the destroyStrategy for
835+
an entire form, add it to the [globalOptions](#global-options)
836+
837+
827838
828839
829840
Specific options and types

src/directives/decorators/bootstrap/checkboxes.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
sf-changed="form"
1010
class="{{form.fieldHtmlClass}}"
1111
ng-model="titleMapValues[$index]"
12-
schema-validate="form"
12+
schema-vaidate="form"
1313
name="{{form.key.slice(-1)[0]}}">
1414
<span ng-bind-html="form.titleMap[$index].name"></span>
1515
</label>

src/directives/decorators/bootstrap/textarea.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="form-group has-feedback {{form.htmlClass}} schema-form-textarea" ng-class="{'has-error': form.disableErrorState !== true && hasError(), 'has-success': form.disableSuccessState !== true && hasSuccess()}">
2-
<label class="control-label {{form.labelHtmlClass}}" ng-class="{'sr-only': !showTitle()}" for="{{form.key.slice(-1)[0]}}">{{form.title}}</label>
2+
<label class="{{form.labelHtmlClass}}" ng-class="{'sr-only': !showTitle()}" for="{{form.key.slice(-1)[0]}}">{{form.title}}</label>
33

44
<textarea ng-if="!form.fieldAddonLeft && !form.fieldAddonRight"
55
class="form-control {{form.fieldHtmlClass}}"

src/directives/schema-form.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ FIXME: real documentation
55

66
angular.module('schemaForm')
77
.directive('sfSchema',
8-
['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath',
9-
function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath) {
8+
['$compile', 'schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfRetainModel',
9+
function($compile, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfRetainModel) {
1010

1111
var SNAKE_CASE_REGEXP = /[A-Z]/g;
1212
var snakeCase = function(name, separator) {
@@ -161,6 +161,9 @@ angular.module('schemaForm')
161161
}
162162
});
163163

164+
scope.$on('$destroy', function() {
165+
sfRetainModel.setFlag(true);
166+
});
164167
}
165168
};
166169
}

src/directives/schema-validate.js

+174-94
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,190 @@
1-
angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', function(sfValidator, sfSelect) {
2-
return {
3-
restrict: 'A',
4-
scope: false,
5-
// We want the link function to be *after* the input directives link function so we get access
6-
// the parsed value, ex. a number instead of a string
7-
priority: 500,
8-
require: 'ngModel',
9-
link: function(scope, element, attrs, ngModel) {
10-
11-
12-
// We need the ngModelController on several places,
13-
// most notably for errors.
14-
// So we emit it up to the decorator directive so it can put it on scope.
15-
scope.$emit('schemaFormPropagateNgModelController', ngModel);
16-
17-
var error = null;
18-
19-
var getForm = function() {
20-
if (!form) {
21-
form = scope.$eval(attrs.schemaValidate);
22-
}
23-
return form;
24-
};
25-
var form = getForm();
26-
if (form.copyValueTo) {
27-
ngModel.$viewChangeListeners.push(function() {
28-
var paths = form.copyValueTo;
29-
angular.forEach(paths, function(path) {
30-
sfSelect(path, scope.model, ngModel.$modelValue);
1+
angular.module('schemaForm').directive('schemaValidate', ['sfValidator', 'sfSelect', 'sfUnselect', '$parse', 'sfRetainModel',
2+
function(sfValidator, sfSelect, sfUnselect, $parse, sfRetainModel) {
3+
4+
return {
5+
restrict: 'A',
6+
scope: false,
7+
// We want the link function to be *after* the input directives link function so we get access
8+
// the parsed value, ex. a number instead of a string
9+
priority: 500,
10+
require: 'ngModel',
11+
link: function(scope, element, attrs, ngModel) {
12+
13+
// We need the ngModelController on several places,
14+
// most notably for errors.
15+
// So we emit it up to the decorator directive so it can put it on scope.
16+
scope.$emit('schemaFormPropagateNgModelController', ngModel);
17+
18+
var error = null;
19+
20+
var getForm = function() {
21+
if (!form) {
22+
form = scope.$eval(attrs.schemaValidate);
23+
}
24+
return form;
25+
};
26+
var form = getForm();
27+
if (form.copyValueTo) {
28+
ngModel.$viewChangeListeners.push(function() {
29+
var paths = form.copyValueTo;
30+
angular.forEach(paths, function(path) {
31+
sfSelect(path, scope.model, ngModel.$modelValue);
32+
});
3133
});
32-
});
33-
}
34+
}
3435

35-
// Validate against the schema.
36+
// Validate against the schema.
3637

37-
var validate = function(viewValue) {
38-
form = getForm();
39-
//Still might be undefined
40-
if (!form) {
41-
return viewValue;
42-
}
38+
var validate = function(viewValue) {
39+
form = getForm();
40+
//Still might be undefined
41+
if (!form) {
42+
return viewValue;
43+
}
4344

44-
// Omit TV4 validation
45-
if (scope.options && scope.options.tv4Validation === false) {
46-
return viewValue;
47-
}
45+
// Omit TV4 validation
46+
if (scope.options && scope.options.tv4Validation === false) {
47+
return viewValue;
48+
}
4849

49-
var result = sfValidator.validate(form, viewValue);
50-
// Since we might have different tv4 errors we must clear all
51-
// errors that start with tv4-
52-
Object.keys(ngModel.$error)
50+
var result = sfValidator.validate(form, viewValue);
51+
// Since we might have different tv4 errors we must clear all
52+
// errors that start with tv4-
53+
Object.keys(ngModel.$error)
5354
.filter(function(k) { return k.indexOf('tv4-') === 0; })
5455
.forEach(function(k) { ngModel.$setValidity(k, true); });
5556

56-
if (!result.valid) {
57-
// it is invalid, return undefined (no model update)
58-
ngModel.$setValidity('tv4-' + result.error.code, false);
59-
error = result.error;
60-
return undefined;
57+
if (!result.valid) {
58+
// it is invalid, return undefined (no model update)
59+
ngModel.$setValidity('tv4-' + result.error.code, false);
60+
error = result.error;
61+
return undefined;
62+
}
63+
return viewValue;
64+
};
65+
66+
// Custom validators, parsers, formatters etc
67+
if (typeof form.ngModel === 'function') {
68+
form.ngModel(ngModel);
6169
}
62-
return viewValue;
63-
};
6470

65-
// Custom validators, parsers, formatters etc
66-
if (typeof form.ngModel === 'function') {
67-
form.ngModel(ngModel);
68-
}
71+
['$parsers', '$viewChangeListeners', '$formatters'].forEach(function(attr) {
72+
if (form[attr] && ngModel[attr]) {
73+
form[attr].forEach(function(fn) {
74+
ngModel[attr].push(fn);
75+
});
76+
}
77+
});
6978

70-
['$parsers', '$viewChangeListeners', '$formatters'].forEach(function(attr) {
71-
if (form[attr] && ngModel[attr]) {
72-
form[attr].forEach(function(fn) {
73-
ngModel[attr].push(fn);
74-
});
75-
}
76-
});
79+
['$validators', '$asyncValidators'].forEach(function(attr) {
80+
// Check if our version of angular has i, i.e. 1.3+
81+
if (form[attr] && ngModel[attr]) {
82+
angular.forEach(form[attr], function(fn, name) {
83+
ngModel[attr][name] = fn;
84+
});
85+
}
86+
});
7787

78-
['$validators', '$asyncValidators'].forEach(function(attr) {
79-
// Check if our version of angular has i, i.e. 1.3+
80-
if (form[attr] && ngModel[attr]) {
81-
angular.forEach(form[attr], function(fn, name) {
82-
ngModel[attr][name] = fn;
83-
});
84-
}
85-
});
86-
87-
// Get in last of the parses so the parsed value has the correct type.
88-
// We don't use $validators since we like to set different errors depeding tv4 error codes
89-
ngModel.$parsers.push(validate);
90-
91-
// Listen to an event so we can validate the input on request
92-
scope.$on('schemaFormValidate', function() {
93-
if (ngModel.$setDirty) {
94-
// Angular 1.3+
95-
ngModel.$setDirty();
96-
validate(ngModel.$modelValue);
97-
} else {
98-
// Angular 1.2
99-
ngModel.$setViewValue(ngModel.$viewValue);
88+
// Get in last of the parses so the parsed value has the correct type.
89+
// We don't use $validators since we like to set different errors depeding tv4 error codes
90+
ngModel.$parsers.push(validate);
91+
92+
// Listen to an event so we can validate the input on request
93+
scope.$on('schemaFormValidate', function() {
94+
if (ngModel.$setDirty) {
95+
// Angular 1.3+
96+
ngModel.$setDirty();
97+
validate(ngModel.$modelValue);
98+
} else {
99+
// Angular 1.2
100+
ngModel.$setViewValue(ngModel.$viewValue);
101+
}
102+
103+
});
104+
105+
106+
var DEFAULT_DESTROY_STRATEGY = getGlobalOptionsDestroyStrategy();
107+
108+
function getGlobalOptionsDestroyStrategy() {
109+
var defaultStrategy = undefined;
110+
if (scope.options && scope.options.hasOwnProperty('destroyStrategy')) {
111+
var globalOptionsDestroyStrategy = scope.options.destroyStrategy;
112+
var isValidFormDefaultDestroyStrategy = (globalOptionsDestroyStrategy === undefined ||
113+
globalOptionsDestroyStrategy === '' ||
114+
globalOptionsDestroyStrategy === null ||
115+
globalOptionsDestroyStrategy === 'retain');
116+
if (isValidFormDefaultDestroyStrategy) {
117+
defaultStrategy = globalOptionsDestroyStrategy;
118+
}
119+
else {
120+
console.warn('Unrecognized globalOptions destroyStrategy: %s \'%s\'. Used undefined instead.',
121+
typeof globalOptionsDestroyStrategy, globalOptionsDestroyStrategy);
122+
}
123+
}
124+
return defaultStrategy;
100125
}
101126

102-
});
127+
// Clean up the model when the corresponding form field is $destroy-ed.
128+
// Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition.
129+
scope.$on('$destroy', function() {
130+
131+
var form = getForm();
132+
var conditionResult = $parse(form.condition);
133+
var formModelNotRetained = !sfRetainModel.getFlag();
134+
135+
// If condition is defined and not satisfied and the sfSchema model should not be retained.
136+
if (form.hasOwnProperty('condition') && !conditionResult(scope) && formModelNotRetained) {
137+
138+
// Either set in form definition, or as part of globalOptions.
139+
var destroyStrategy =
140+
!form.hasOwnProperty('destroyStrategy') ? DEFAULT_DESTROY_STRATEGY : form.destroyStrategy;
141+
var schemaType = getSchemaType();
142+
143+
if (destroyStrategy && destroyStrategy !== 'retain') {
144+
// Don't recognize the strategy, so give a warning.
145+
console.warn('%s has defined unrecognized destroyStrategy: \'%s\'. Used default instead.',
146+
attrs.name, destroyStrategy);
147+
destroyStrategy = DEFAULT_DESTROY_STRATEGY;
148+
}
149+
else if (schemaType !== 'string' && destroyStrategy === '') {
150+
// Only 'string' type fields can have an empty string value as a valid option.
151+
console.warn('%s attempted to use empty string destroyStrategy on non-string form type. ' +
152+
'Used default instead.', attrs.name);
153+
destroyStrategy = DEFAULT_DESTROY_STRATEGY;
154+
}
155+
156+
if (destroyStrategy === 'retain') {
157+
return; // Valid option to avoid destroying data in the model.
158+
}
159+
160+
destroyUsingStrategy(destroyStrategy);
161+
162+
function destroyUsingStrategy(strategy) {
163+
var strategyIsDefined = (strategy === null || strategy === '' || strategy === undefined);
164+
if (!strategyIsDefined) {
165+
strategy = DEFAULT_DESTROY_STRATEGY;
166+
}
167+
sfUnselect(scope.form.key, scope.model, strategy);
168+
}
169+
170+
function getSchemaType() {
171+
var sType;
172+
if (form.schema) {
173+
sType = form.schema.type;
174+
}
175+
else {
176+
sType = null;
177+
}
178+
return sType;
179+
}
180+
}
181+
});
182+
103183

104-
scope.schemaError = function() {
105-
return error;
106-
};
184+
scope.schemaError = function() {
185+
return error;
186+
};
107187

108-
}
109-
};
110-
}]);
188+
}
189+
};
190+
}]);

src/services/retainModel.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
angular.module('schemaForm').factory('sfRetainModel', function() {
2+
3+
var data = {retainModelFlag: false };
4+
5+
return {
6+
/**
7+
* @description
8+
* Utility service to indicate if the sfSchema model should be retained.
9+
* Set to true to prevent an operation that would have destroyed the model
10+
* from doing so (such as wrapping the form in an ng-if).
11+
*
12+
* ex.
13+
* var foo = sfRetainModel.getFlag();
14+
*
15+
* @returns {boolean} returns the current value of the retainModelFlag.
16+
*/
17+
getFlag: function () {
18+
return data.retainModelFlag;
19+
},
20+
21+
/**
22+
* @description
23+
* Set the value of the retainModelFlag.
24+
* True prevents cleaning the data in the model, while false follows the configured destroyStrategy.
25+
*
26+
* ex.
27+
* var bar = sfRetainModel.setFlag(true);
28+
*
29+
* @param {boolean} value The boolean value to set as the retainModelFlag
30+
* @returns {boolean} returns the value of the retainModelFlag after toggling.
31+
*/
32+
setFlag: function(value) {
33+
data.retainModelFlag = value;
34+
return data.retainModelFlag;
35+
}
36+
}
37+
});

0 commit comments

Comments
 (0)