Skip to content

Commit d21377f

Browse files
committed
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.
1 parent dac6d83 commit d21377f

File tree

3 files changed

+248
-94
lines changed

3 files changed

+248
-94
lines changed

docs/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,15 @@ function FormCtrl($scope) {
823823
Note that arrays inside arrays won't work with conditions.
824824
825825
826+
### destroyStrategy
827+
By default, when a field is removed from the DOM and the $destroy event is broadcast, the schema-validate directive
828+
will update the model to set the field value to undefined. This can be overridden by setting the destroyStrategy
829+
on a field to one of null, empty string (""), undefined, or "retain". Any other value will be ignored and the default
830+
behavior will apply. The empty string option only applies to fields that have a type of string; using the empty string
831+
with other field types will just be set to the default destroyStrategy. If you'd like to set the destroyStrategy for
832+
an entire form, add it to the formDefaults in the [globalOptions](#global-options)
833+
834+
826835
827836
828837
Specific options and types

src/directives/schema-validate.js

Lines changed: 157 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,173 @@
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',
2+
function(sfValidator, sfSelect, sfUnselect) {
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;
107+
if (scope.options && scope.options.formDefaults) {
108+
var formDefaultDestroyStrategy = scope.options.formDefaults.destroyStrategy;
109+
var isValidFormDefaultDestroyStrategy = (formDefaultDestroyStrategy === undefined ||
110+
formDefaultDestroyStrategy === '' ||
111+
formDefaultDestroyStrategy === null ||
112+
formDefaultDestroyStrategy === 'retain');
113+
if (isValidFormDefaultDestroyStrategy) {
114+
DEFAULT_DESTROY_STRATEGY = formDefaultDestroyStrategy;
115+
}
116+
else {
117+
console.warn('Unrecognized formDefaults.destroyStrategy: \'%s\'. Used undefined instead.',
118+
formDefaultDestroyStrategy);
119+
DEFAULT_DESTROY_STRATEGY = undefined;
120+
}
100121
}
101122

102-
});
123+
// Clean up the model when the corresponding form field is $destroy-ed.
124+
// Default behavior can be supplied as a formDefault, and behavior can be overridden in the form definition.
125+
scope.$on('$destroy', function() {
126+
var form = getForm();
127+
var destroyStrategy = form.destroyStrategy; // Either set in form definition, or as part of formDefaults.
128+
var schemaType = getSchemaType();
129+
130+
if (destroyStrategy && destroyStrategy !== 'retain' ) {
131+
// Don't recognize the strategy, so give a warning.
132+
console.warn('Unrecognized destroyStrategy: \'%s\'. Used default instead.', destroyStrategy);
133+
destroyStrategy = DEFAULT_DESTROY_STRATEGY;
134+
}
135+
else if (schemaType !== 'string' && destroyStrategy === '') {
136+
// Only 'string' type fields can have an empty string value as a valid option.
137+
console.warn('Attempted to use empty string destroyStrategy on non-string form type. Used default instead.');
138+
destroyStrategy = DEFAULT_DESTROY_STRATEGY;
139+
}
140+
141+
if (destroyStrategy === 'retain') {
142+
return; // Valid option to avoid destroying data in the model.
143+
}
144+
145+
destroyUsingStrategy(destroyStrategy);
146+
147+
function destroyUsingStrategy(strategy) {
148+
var strategyIsDefined = (strategy === null || strategy === '' || typeof strategy == undefined);
149+
if (!strategyIsDefined){
150+
strategy = DEFAULT_DESTROY_STRATEGY;
151+
}
152+
sfUnselect(scope.form.key, scope.model, strategy);
153+
}
154+
155+
function getSchemaType() {
156+
if (form.schema) {
157+
schemaType = form.schema.type;
158+
}
159+
else {
160+
schemaType = null;
161+
}
162+
}
163+
});
164+
165+
103166

104-
scope.schemaError = function() {
105-
return error;
106-
};
167+
scope.schemaError = function() {
168+
return error;
169+
};
107170

108-
}
109-
};
110-
}]);
171+
}
172+
};
173+
}]);

src/services/unselect.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
angular.module('schemaForm').factory('sfUnselect', ['sfPath', function(sfPath) {
2+
var numRe = /^\d+$/;
3+
4+
/**
5+
* @description
6+
* Utility method to clear deep properties without
7+
* throwing errors when things are not defined.
8+
* DOES NOT create objects when they are missing.
9+
*
10+
* Based on sfSelect.
11+
*
12+
* ex.
13+
* var foo = Unselect('address.contact.name',obj, null)
14+
* var bar = Unselect('address.contact.name',obj, undefined)
15+
* Unselect('address.contact.name',obj,'')
16+
*
17+
* @param {string} projection A dot path to the property you want to set
18+
* @param {object} obj (optional) The object to project on, defaults to 'this'
19+
* @param {Any} unselectValue The value to set; if parts of the path of
20+
* the projection is missing empty objects will NOT be created.
21+
* @returns {Any|undefined} returns the value at the end of the projection path
22+
* or undefined if there is none.
23+
*/
24+
return function(projection, obj, unselectValue) {
25+
if (!obj) {
26+
obj = this;
27+
}
28+
//Support [] array syntax
29+
var parts = typeof projection === 'string' ? sfPath.parse(projection) : projection;
30+
//console.log(parts);
31+
32+
if (parts.length === 1) {
33+
//Special case, just setting one variable
34+
35+
//console.log('Only 1 variable in parts');
36+
obj[parts[0]] = unselectValue;
37+
return obj;
38+
}
39+
40+
if (typeof obj[parts[0]] === 'undefined') {
41+
// If top-level part isn't defined.
42+
var isArray = numRe.test(parts[1]);
43+
if (isArray) {
44+
//console.info('Expected array as top-level part, but is already undefined. Returning.');
45+
return undefined;
46+
}
47+
else if (parts.length > 2) {
48+
obj[parts[0]] = {};
49+
}
50+
}
51+
52+
var value = obj[parts[0]];
53+
for (var i = 1; i < parts.length; i++) {
54+
// Special case: We allow JSON Form syntax for arrays using empty brackets
55+
// These will of course not work here so we exit if they are found.
56+
if (parts[i] === '') {
57+
return undefined;
58+
}
59+
60+
var tmp = value[parts[i]];
61+
if (i === parts.length - 1 ) {
62+
//End of projection; setting the value
63+
64+
//console.log('Value set using destroyStrategy.');
65+
value[parts[i]] = unselectValue;
66+
return unselectValue;
67+
} else {
68+
// Make sure to NOT create new objects on the way if they are not there.
69+
// We need to look ahead to check if array is appropriate.
70+
// Believe that if an array/object isn't present/defined, we can return.
71+
72+
//console.log('Processing part %s', parts[i]);
73+
if (typeof tmp === 'undefined' || tmp === null) {
74+
//console.log('Part is undefined; returning.');
75+
return undefined;
76+
}
77+
value = tmp;
78+
}
79+
}
80+
return value;
81+
};
82+
}]);

0 commit comments

Comments
 (0)