Skip to content

Commit ffab373

Browse files
committed
Merge branch 'jbsaff-feature/cleanModelUsingDestroyStrategy' into development
2 parents 503abba + 870a2e5 commit ffab373

File tree

7 files changed

+502
-100
lines changed

7 files changed

+502
-100
lines changed

docs/index.md

+20
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: "remove" // One of "null", "empty" , "remove", or 'retain'. Changes model on $destroy event. default is "remove".
638640
}
639641
```
640642
@@ -824,6 +826,24 @@ 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, this happens
831+
if you use the `condition` option, the schema-validate directive will update the model to set the
832+
field value to `undefined`. This can be overridden by setting the destroyStrategy on a field, or as a
833+
global option, to one of the strings `"null"`, `"empty"` , `"remove"`, or `"retain"`.
834+
835+
`"null"` means that model values will be set to `null` instead of being removed.
836+
837+
`"empty"` means empty strings, `""`, for model values that has the `string` type, `{}` for model
838+
values with `object` type and `[]` for `array` type. All other types will be treated as `"remove"`.
839+
840+
`"remove"` deletes the property. This is the default.
841+
842+
`"retain"` keeps the value of the property event though the field is no longer in the form or being
843+
vaidated before submit.
844+
845+
If you'd like to set the destroyStrategy for
846+
an entire form, add it to the [globalOptions](#global-options)
827847
828848
829849
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

+12
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ angular.module('schemaForm')
7272
// they have been removed from the DOM
7373
// https://github.com/Textalk/angular-schema-form/issues/200
7474
if (childScope) {
75+
// Destroy strategy should not be acted upon
76+
scope.externalDestructionInProgress = true;
7577
childScope.$destroy();
78+
scope.externalDestructionInProgress = false;
7679
}
7780
childScope = scope.$new();
7881

@@ -161,6 +164,15 @@ angular.module('schemaForm')
161164
}
162165
});
163166

167+
scope.$on('$destroy', function() {
168+
// Each field listens to the $destroy event so that it can remove any value
169+
// from the model if that field is removed from the form. This is the default
170+
// destroy strategy. But if the entire form (or at least the part we're on)
171+
// gets removed, like when routing away to another page, then we definetly want to
172+
// keep the model intact. So therefore we set a flag to tell the others it's time to just
173+
// let it be.
174+
scope.externalDestructionInProgress = true;
175+
});
164176
}
165177
};
166178
}

src/directives/schema-validate.js

+96-95
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,111 @@
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', '$parse',
2+
function(sfValidator, $parse) {
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);
100-
}
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+
}
101102

102-
});
103+
});
103104

104-
scope.schemaError = function() {
105-
return error;
106-
};
105+
scope.schemaError = function() {
106+
return error;
107+
};
107108

108-
}
109-
};
110-
}]);
109+
}
110+
};
111+
}]);

src/services/decorators.js

+46-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ angular.module('schemaForm').provider('schemaFormDecorators',
3131

3232
var createDirective = function(name) {
3333
$compileProvider.directive(name,
34-
['$parse', '$compile', '$http', '$templateCache', '$interpolate', '$q', 'sfErrorMessage', 'sfPath',
35-
function($parse, $compile, $http, $templateCache, $interpolate, $q, sfErrorMessage, sfPath) {
34+
['$parse', '$compile', '$http', '$templateCache', '$interpolate', '$q', 'sfErrorMessage',
35+
'sfPath','sfSelect',
36+
function($parse, $compile, $http, $templateCache, $interpolate, $q, sfErrorMessage,
37+
sfPath, sfSelect) {
3638

3739
return {
3840
restrict: 'AE',
@@ -261,7 +263,48 @@ angular.module('schemaForm').provider('schemaFormDecorators',
261263
scope.$broadcast('schemaFormValidate');
262264
}
263265
}
264-
})
266+
});
267+
268+
// Clean up the model when the corresponding form field is $destroy-ed.
269+
// Default behavior can be supplied as a globalOption, and behavior can be overridden in the form definition.
270+
scope.$on('$destroy', function() {
271+
// If the entire schema form is destroyed we don't touch the model
272+
if (!scope.externalDestructionInProgress) {
273+
var destroyStrategy = form.destroyStrategy ||
274+
(scope.options && scope.options.destroyStrategy) || 'remove';
275+
// No key no model, and we might have strategy 'retain'
276+
if (form.key && destroyStrategy !== 'retain') {
277+
278+
// Get the object that has the property we wan't to clear.
279+
var obj = scope.model;
280+
if (form.key.length > 1) {
281+
obj = sfSelect(form.key.slice(0, form.key.length - 1), obj);
282+
}
283+
284+
// We can get undefined here if the form hasn't been filled out entirely
285+
if (obj === undefined) {
286+
return;
287+
}
288+
289+
// Type can also be a list in JSON Schema
290+
var type = (form.schema && form.schema.type) || '';
291+
292+
// Empty means '',{} and [] for appropriate types and undefined for the rest
293+
//console.log('destroy', destroyStrategy, form.key, type, obj);
294+
if (destroyStrategy === 'empty' && type.indexOf('string') !== -1) {
295+
obj[form.key.slice(-1)] = '';
296+
} else if (destroyStrategy === 'empty' && type.indexOf('object') !== -1) {
297+
obj[form.key.slice(-1)] = {};
298+
} else if (destroyStrategy === 'empty' && type.indexOf('array') !== -1) {
299+
obj[form.key.slice(-1)] = [];
300+
} else if (destroyStrategy === 'null') {
301+
obj[form.key.slice(-1)] = null;
302+
} else {
303+
delete obj[form.key.slice(-1)];
304+
}
305+
}
306+
}
307+
});
265308
}
266309

267310
once();

0 commit comments

Comments
 (0)