/*
* 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
*
*/
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
*
*
* 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):
*
*
*
* When within a repeater where scope is an entity:
*
*
* 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('');
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 =
* '' +
* 'Oh No!!! %error%';
* }]);
*
* // ... or during the app's Angular run phase:
* app.run(['zDirectivesConfig', function(cfg) {
* cfg.zValidateTemplate =
* '' +
* 'So sad!!! %error%';
* }]);
*/
function zDirectivesConfig() {
// The default zValidate template for display of validation errors
this.zValidateTemplate =
'%error%';
// The default template for indicating required fields.
// Assumes "icon-asterisk-invalid" from bootstrap css
this.zRequiredTemplate =
'*';
this.$get = function () {
return {
zValidateTemplate: this.zValidateTemplate,
zRequiredTemplate: this.zRequiredTemplate
};
};
};
})();