-
Notifications
You must be signed in to change notification settings - Fork 1
/
ValidatedViewModel.js
executable file
·321 lines (282 loc) · 13.8 KB
/
ValidatedViewModel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/**
* This function creates a ValidatedViewModel Class by adding the appropriate methods.
* ValidatedViewModel Classes permit you to apply context-sensitive constraintGroups
* to instances of the class.
*
* @param function
* viewModel the POJO Knockout-JS style viewModel function that you
* would normally write. This object must already have
* ko.observable*s|computed's assigned to the desired properties.
*
* In addition, the function must contain a publicly-accesible
* property called 'constraintGroups'. The property must be a map of
* group names to constraint group definitions. Constraint group
* definitions are maps of property names to constraint definitions.
* Constraint definitions are objects identical to that which would
* be passed as an argument to a
* (implements ko.subscribable()).extend() for the
* Knockout-Validation plugin.
*
* @url https://github.com/ericmbarnard/Knockout-Validation/wiki/Native-Rules
* @author [email protected]
* @example
* // -------------------------------------------------------------------
* //src: myModelDefinitions.js
*
* var MyViewModel = ValidatedViewModel(function(){
* var self = this;
* self.prop1 = ko.observable();
* self.prop2 = ko.computed(function(...){...});
* self.prop3 = ko.observableArray();
*
* self.constraintGroups = {
* yourGroupNameHere :
* {
* prop1 : {required: true},
* prop2 : {pattern: '^[a-z0-9].$'},
* prop3 : {minLength: 3, message:'insufficient length'}
* }
* };
*
* });
* // -------------------------------------------------------------------
* //src: myPageSpecificStuff.js
*
* //must run this method before applying any constraint groups
* ko.validation.init();
* var instanceOfMyViewModel = new MyViewModel();
* instanceofMyViewModel.applyConstraintGroup('yourGroupNameHere');
* //rest of validation code here
* // -------------------------------------------------------------------
*/
var ValidatedViewModel = function (ViewModelFunc) {
if ('function' !== typeof ViewModelFunc) {
throw new TypeError(
"the supplied viewModel must be of type function. '" + typeof viewModel + "' given.");
}
/**
* @deprecated - this method was a safe way of updating the (property
* name -> constraint) map. This function now assumes that
* the developer will properly format self.constraintGroups
*
* registers a map of property names to constraint specification
* objects. Fails if a group of the same name has already been
* registered, or if the given name is not a string.
*
* @param string
* name for the constraint group
* @param propToConstraintMap -
* a map of view model property names to objects containing
* their validation constraint specifications. The
* specification must be identical to that which would be
* passed as an argument to ko.observable().extend() for the
* Knockout-Validation plugin.
*
* @url https://github.com/ericmbarnard/Knockout-Validation/wiki/Native-Rules
*
* @returns void
* @author [email protected]
*/
/*
* var addConstraintGroup = function(name, propToConstraintMap){
* if("undefined" == typeof self.constraintGroups[name]){//if key is yet
* unused if(name instanceof String || name.constructor === String){
* self.constraintGroups[name] = propToConstraintMap; } else{ throw new
* TypeError("'name' must be of type String." + typeof name +" given."); } }
* else{ //key is already used throw new Error("a constraint group named '" +
* name + "' already exists"); } };
*/
/**
* Applies the specified validation constraint group to the view model. This
* function may not be called in your viewModel's constructor. The
* application of multiple constraint groups does not eliminate or overwrite
* the constraint definitions of preiviously applied constraint groups.
*
* @param name
* string - name of the view model
*
* @param config
* Object - optional argument that is passed as the second
* argument to ko.validation.group();
* @url https://github.com/ericmbarnard/Knockout-Validation/wiki/Configuration
* (see Option "grouping")
* @returns void
* @author [email protected]
*/
ViewModelFunc.prototype.applyConstraintGroup = function (name, config) {
if (name instanceof String || name.constructor === String) {
if ("undefined" === typeof this.constraintGroups || 'object' != typeof this.constraintGroups) {
throw new Error(
"Validated View Models must define a this.constraintGroups object. No such object detected.");
}
if ("undefined" === typeof this.constraintGroups[name]) { // if key is
// yet
// registered
throw new Error(
"This view model has no registered constraint group named '" + name + "'. Correct the name or register a new constraint group in this.constraintGroups");
} else {
//initialize if necessary::
if ('undefined' === typeof this.appliedConstraintGroups) {
this.appliedConstraintGroups = new Array();
}
//check for previous application of group
if (name in this.appliedConstraintGroups) {
// if group has already been applied,
throw new Error("The Constraint Group '" + name + "' has already been applied to this model.");
}
var constraintGroupId = ko.validation.utils.newId();
this.appliedConstraintGroups[name] = constraintGroupId;
for (propertyName in this.constraintGroups[name]) {
var prop = this[propertyName];
if ('undefined' == typeof prop) {
throw new Error("Property '" + propertyName + "' was not found in this view model");
}
if (!(prop instanceof ko.subscribable.constructor)) {
// if this is not a ko.observable(Array) or a
// ko.computed
throw new TypeError(
"Property '" + propertyName + "' must be a ko.observable(Array) or a ko.computed to be validated.");
}
var constraints = this.constraintGroups[name][propertyName];
for(var constraintName in constraints){
if(constraints.hasOwnProperty(constraintName)){
var constraintDefinition = constraints[constraintName];
if('object' == typeof constraintDefinition){
//if the definition contains messages etc., but
//no parameter to the validator
//was specified, insert true for params
//value. This is identical to ko.validation behavior
if('undefined' === typeof constraintDefinition.params){
constraintDefinition.params = true;
}
}
else{ //parameter was a primitive, make it an object
constraintDefinition = {params: constraintDefinition};
}
//now that the constraint definition is definitely an object in the
//proper format add key to the object
constraintDefinition.constraintGroupId = constraintGroupId;
constraints[constraintName] = constraintDefinition;
}//end if
}//end foreach
// add the constraints to the view model property
prop.extend(constraints);
};
this['errors'] = ko.validation.group(this, config);
}
} else {
throw new TypeError(
"a string must be used to specify a registered constraint");
}
};
/**
* Applies all of the specified constraint groups to the model in the same
* order that they were specified.
*
* @param groupNames
* array of string constraint group names
* @param config
* object - see applyConstraintGroup documentation
* @author [email protected]
*/
ViewModelFunc.prototype.applyConstraintGroups = function (groupNames, config) {
for (var i = 0; i < groupNames.length; ++i) {
this.applyConstraintGroup(groupNames[i], config);
}
};
/**
* Removes the specified constraint from the specified property
*
* @param constraintName
* string - the name of the constraint. Ex: "required",
* "max", etc.
*
* @param constraintParameters
* mixed - the value of the parameter passed to the
* constraint. Ex: 'true' in {required: true}. '42' in {max:
* 42}
*
* @param propertyName
* string- the name of the property to remove the constraint
* from
*
* @returns boolean - true if the constriant 'constraintName' was
* deleted from property 'propertyName'. false if the
* constraint was not found.
* @author [email protected]
*/
ViewModelFunc.prototype.removeConstraintFromProperty = function (
constraintName, constraintParameters, propertyName) {
if (this[propertyName] instanceof ko.subscribable.constructor) {
var rules = this[propertyName].rules();
} else {
throw new Error("Property '" + propertyName + "' does not implement ko.subscribable");
}
if ('undefined' === typeof rules) {
throw new ReferenceError("Property '" + propertyName + "' does not exist");
}
var found = false;
for (var i = 0; i < rules.length; i++) {
if (rules[i].rule === constraintName && rules[i].params == constraintParameters) {
found = true;
rules.splice(i, 1);
break;
// don't continue deleting any additional occurences of this
// constraint. Hence if applyConstraintGroups permitted multiple
// applications of constraint groups, N applications of the
// same constraint group would require N calls to
// removeConstraintGroup
}
}
return found;
};
/**
* Removes the named constraint group. Throws errors if constraint group
* does not not exist, or if the constraint group is not currently applied.
*
* @param constraintGroupName
* string constraint group name
* @author [email protected]
*/
ViewModelFunc.prototype.removeConstraintGroup = function (constraintGroupName) {
//ensure constraintGroups are still defined
if ('undefined' === typeof this.constraintGroups) {
throw new ReferenceError('Cannot remove the constraint group- no constraint groups have been defined');
}
//create local alias
var constraintGroup = this.constraintGroups[constraintGroupName];
//check for bad constraint group name
if ('undefined' === typeof constraintGroup) {
throw new ReferenceError("Constraint group '" + constraintGroupName + "' does not exist");
}
//now that we know the constraint group exists, check to see if it has been applied
//after checking to see that the appliedConstraintGroups property exists:
if ('undefined' === typeof this.appliedConstraintGroups) {
//then no constraintGroups have been applied
throw new Error("Constraint group '" + constraintGroupName + "' is not currently applied.");
}
if (!(constraintGroupName in this.appliedConstraintGroups)) {
throw new Error("Constraint group '" + constraintGroupName + "' is not currently applied.");
}
var constraintGroupId = this.appliedConstraintGroups[constraintGroupName];
for (propertyName in constraintGroup) {
var rulesForProperty = this[propertyName].rules();
for (var i = 0; i < rulesForProperty.length; i++) {
if (rulesForProperty[i].constraintGroupId === constraintGroupId) {
//delete
rulesForProperty.splice(i, 1);
//update counter to account for delete
i--;
}
}
//now force validation to occur again
//unfortunately, this inefficiently notifies all
//subscribers instead of just the validation handlers
//@todo only trigger the subscribed validation handlers
this[propertyName].notifySubscribers(this[propertyName]());
}
delete this.appliedConstraintGroups[constraintGroupName];
};
// return the newly-equipped View Model "class"
return ViewModelFunc;
};