forked from Breeze/breeze.js.labs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
breeze.directives.js
420 lines (370 loc) · 16.6 KB
/
breeze.directives.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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
/*
* Breeze Labs: Breeze Directives for Angular Apps
*
* v.1.3.5
*
* Usage:
* Make this module a dependency of your app module:
* var app = angular.module('app', ['breeze.directives']);
*
* Copyright 2014 IdeaBlade, Inc. All Rights Reserved.
* Licensed under the MIT License
* http://opensource.org/licenses/mit-license.php
* Author: Ward Bell
*/
(function () {
'use strict';
var module = angular.module('breeze.directives', [])
.directive('zFloat', [zFloat])
.directive('zValidate', ['zDirectivesConfig', 'zValidateInfo', zValidate])
.service('zValidateInfo', zValidateInfo)
.provider('zDirectivesConfig', zDirectivesConfig);
/*** IMPLEMENTATION ***/
/* Breeze Float Equivalence directive
*
* Adds a formatter to the ngModel controller.
* This formatter returns the view value rather than the model property value
* if the two values are deemed equivalent.
*
* For explanation and more info, see
* http://www.breezejs.com/breeze-labs/breezedirectivesfloat
*
* Install
* --------------------------------------------------
*
* Make this module a dependency of your app module:
* var app = angular.module('app', ['breeze.directives']);
*
* Add the directive to an input tag bound to a floating point property
* <input data-ng-model='vm.product.unitPrice' data-z-float />
*/
function zFloat() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
ngModelCtrl.$formatters.push(equivalenceFormatter);
function equivalenceFormatter(value){
var viewValue = ngModelCtrl.$viewValue // could have used 'elm.val()'
return (value === +viewValue) ? viewValue : value;
}
}
};
}
/* Breeze Validation directive
*
* Displays the model validation errors for an entity property
* and adds required indicator if the bound property is required
*
* Install
* --------------------------------------------------
* Include breeze.directives.css for default styling
* <link href="content/breeze.directives.css" rel="stylesheet" />
*
* Make this module a dependency of your app module:
* var app = angular.module('app', ['breeze.directives']);
*
* Usage for input elements (input|select|textarea):
* ---------------------------------------------------
* When scope is a viewmodel (vm):
* <input data-ng-model='vm.session.firstName' data-z-validate />
* <input data-ng-model='vm.session.track' data-z-validate='trackId' />
*
* When within a repeater where scope is an entity:
* <input data-ng-model='title' data-z-validate />
*
* Required indicator applied if the bound data property
* has a required validator. A required validator is a validator
* which has an validator.context.isRequired == true property (or is named 'required')
* See `zValidateInfo.getRequiredPropertiesForEntityType`
*
* Usage for non-input elements (e.g. a div that formats the required and error msg):
* ---------------------------------------------------
* TBD
*
* Learn more at http://www.breezejs.com/breeze-labs/breezedirectivesvalidationjs
*/
function zValidate(config, validateInfo) {
var directive = {
link: link,
restrict: 'A',
scope: true
};
return directive;
function link(scope, element, attrs) {
// get validation info for bound element and entity property
var info = validateInfo.create(
scope,
attrs.ngModel,
attrs.zValidate);
if (!info.getValErrs) { return; } // can't do anything
// Use only features defined in Angular's jqLite
var domEl = element[0];
var nodeName = domEl.nodeName;
var isInput = nodeName == 'INPUT' || nodeName == 'SELECT' || nodeName == 'TEXTAREA';
isInput ? linkForInput() : linkForNonInput();
// directive is on an input element, so use templates for
// required and validation display
function linkForInput() {
var valTemplate = config.zValidateTemplate;
var requiredTemplate = config.zRequiredTemplate || '';
var decorator = angular.element('<span class="z-decorator"></span>');
element.after(decorator);
// unwrap bound elements
decorator = decorator[0];
scope.$watch(info.getValErrs, valErrsChanged);
// update the message in the validation template
// when a validation error changes on an input control
function valErrsChanged(newValue) {
// HTML5 custom validity
// http://dev.w3.org/html5/spec-preview/constraints.html#the-constraint-validation-api
if (domEl.setCustomValidity) {
/* only works in HTML 5. Maybe should throw if not available. */
domEl.setCustomValidity(newValue);
}
var errorHtml = newValue ? valTemplate.replace(/%error%/, newValue) : "";
var isRequired = info.getIsRequired();
var requiredHtml = isRequired ? requiredTemplate : '';
decorator.innerHTML = (isRequired || !!errorHtml) ? requiredHtml + errorHtml : "";
}
}
// directive is on another element (e.g. a div wrapping the input)
// so set scope variables and let existing elements display validation
// TODO: learn to discover the ngModel in the interior of the element
// rather than oblige developer to repeat it in the ngModel of this element
function linkForNonInput() {
scope.$watch(info.getValErrs, valErrsChanged);
// update the message in the z_invalid and z_error properties in the scope
// when a validation error changes on a non-input control
function valErrsChanged(newValue) {
var errorMsg = newValue ? newValue : "";
scope.z_error = errorMsg;
scope.z_invalid = !!errorMsg;
scope.z_required = info.getIsRequired();
}
}
}
}
// Service to extract validation information from a zValidate data binding
// Although built for Angular, it is designed to be used
// in alternative zValidate directive implementations
function zValidateInfo() {
// Info describing a bound entity's property's validation
// 'scope' is the scope of the binding
// 'scope.$eval(text)' evaluates 'text' in the context of that scope
// 'scope.entityAspect' returns an EntityAspect if the scope is an Entity
// while this is an Ng concept, it could be modeled in other frameworks
//
// 'modelPath' is the entity property's data binding specification
// by default the property from which validation information should be obtained.
//
// 'validationPath' is an alternative specification of the entity property
// from which validation information should be obtained.
function Info(scope, modelPath, validationPath) {
// need some path info from either of these attrs or it's pointless
if (!modelPath && !validationPath) { return; }
this.scope = scope;
setEntityAndPropertyPaths(this, modelPath, validationPath);
// this.entityPath
// this.propertyPath
this.getEntityAspect = this.entityPath ?
getEntityAspectFromEntityPath(this) :
getEntityAspect(this);
this.getValErrs = createGetValErrs(this);
this.isRequired = undefined; // don't know initially
}
Info.prototype = {
constructor: Info,
getIsRequired: getIsRequired,
getType: getType
};
return {
create: create,
};
/*** zValidateInfo implementation ***/
// Create info about the data bound entity property
function create(scope, modelPath, validationPath) {
return new Info(scope, modelPath, validationPath);
}
// Create the 'getValErrs' function that will be watched
function createGetValErrs(info) {
return function () {
var aspect = info.getEntityAspect();
if (aspect) {
var errs = aspect.getValidationErrors(info.propertyPath);
if (errs.length) {
return errs
// concatenate all errors into a single string
.map(function (e) { return e.errorMessage; })
.join('; ');
}
return '';
}
// No data bound entity yet.
// Return something other than a string so that
// watch calls `valErrsChanged` when an entity is bound
return null;
};
}
function getEntityAspect(info) {
return function () {
return info.scope.entityAspect;
}
}
function getEntityAspectFromEntityPath(info) {
return function () {
try { return info.scope.$eval(info.entityPath)['entityAspect']; }
catch (_) { return undefined; }
}
}
// determine if bound property is required.
function getIsRequired() {
var info = this;
if (info.isRequired !== undefined) { return info.isRequired; }
// We don't know if it is required yet.
// Once bound to the entity we can determine whether the data property is required
// Note: Not bound until *second* call to the directive's link function
// which is why you MUST call 'getIsRequired'
// inside 'valErrsChanged' rather than in the link function
var entityType = info.getType();
if (entityType) { // the bound entity is known
var requiredProperties =
getRequiredPropertiesForEntityType(entityType);
return info.isRequired = !!requiredProperties[info.propertyPath];
}
return undefined; // don't know yet
}
function getType() {
var aspect = this.getEntityAspect();
return aspect ? aspect.entity.entityType : null;
}
/*
* getRequiredPropertiesForEntityType
* Returns a hash of property names of properties that are required.
* Creates that hash lazily and adds it to the
* entityType's metadata for easier access by this directive
*/
function getRequiredPropertiesForEntityType(type) {
if (type.custom && type.custom.required) {
return type.custom.required;
}
// Don't yet know the required properties for this type
// Find out now
if (!type.custom) {
type.custom = {};
}
var required = {};
type.custom.required = required;
var props = type.getProperties();
props.forEach(function (prop) {
var vals = prop.validators;
for (var i = vals.length; i--;) {
var val = vals[i];
// Todo: add the 'isRequired' property to breeze.Validator.required validator
if (val.context.isRequired || val.name === 'required') {
required[prop.name] = true;
break;
}
}
});
return required;
}
function setEntityAndPropertyPaths(info, modelPath, validationPath) {
// examples:
// 'productId' // property only
// 'vm.order.delivery' // entity path and property
// 'vm.order["delivery"]' // entity path and indexed property
if (modelPath) {
parsePath(modelPath);
}
// validationPath can override either entity or property path;
// examples:
// 'productId' // property only
// 'vm.order.delivery' // entity path and property
// 'vm.order["delivery"]' // entity path and indexed property
//
// optional ',' syntax as {entity, property} path separator
// so can separate entity path from a complex property path
// examples:
// 'vm.order,address.street' // entity w/ complex prop
// 'vm.order,address[street]' // entity w/ complex indexed prop
if (validationPath) {
// Look for ',' syntax
var paths = validationPath.split(',');
var pPath = paths.pop(); // after ','
var ePath = paths.pop(); // before ','
if (ePath) { info.entityPath = ePath.trim(); }
if (info.entityPath) {
info.propertyPath = pPath;
} else {
// Didn't use ',' syntax and didn't specify entityPath in model.
// Therefore entire path spec must be in pPath; parse it.
parsePath(pPath);
}
}
function parsePath(path) {
if (path[path.length - 1] === ']') {
parseIndexedPaths(path);
} else {
parseDottedPath(path);
}
}
function parseDottedPath(path) {
// ex: 'vm.order.delivery'
// propertyPath should be 'delivery'
// entityPath should be 'vm.order'
paths = path.split('.');
info.propertyPath = paths.pop(); // property is after last '.'
info.entityPath = paths.join('.'); // path to entity is before last '.'
}
// extract paths from strings using square-bracket notation, e.g. 'vm.order[delivery]'
function parseIndexedPaths(path) {
var opensb = path.lastIndexOf('[');
info.entityPath = path.substring(0, opensb); // path to entity is before last [
var propertyPath = path.substring(opensb + 1, path.length - 1); // property is between [ ]
// eval it, in case it's an angular expression
try { var evalPath = info.scope.$eval(propertyPath); }
catch (_) { }
info.propertyPath = evalPath ? evalPath : propertyPath;
}
}
}
/* Configure app to use breeze.directives
*
* Configure breeze directive templates for zValidate
*
* zValidateTemplate: template for display of validation errors
* zRequiredTemplate: template for display of required property indicator
*
* Template configuarion usage:
* Either during the app's Angular config phase ...
* app.config(['zDirectivesConfigProvider', function(cfg) {
* cfg.zValidateTemplate =
* '<span class="invalid"><i class="icon-warning-sign"></i>' +
* 'Oh No!!! %error%</span>';
* }]);
*
* // ... or during the app's Angular run phase:
* app.run(['zDirectivesConfig', function(cfg) {
* cfg.zValidateTemplate =
* '<span class="invalid"><i class="icon-warning-sign"></i>' +
* 'So sad!!! %error%</span>';
* }]);
*/
function zDirectivesConfig() {
// The default zValidate template for display of validation errors
this.zValidateTemplate =
'<span class="invalid">%error%</span>';
// The default template for indicating required fields.
// Assumes "icon-asterisk-invalid" from bootstrap css
this.zRequiredTemplate =
'<span class="icon-asterisk-invalid z-required" title="Required">*</span>';
this.$get = function () {
return {
zValidateTemplate: this.zValidateTemplate,
zRequiredTemplate: this.zRequiredTemplate
};
};
};
})();