Socket
Socket
Sign inDemoInstall

angular-form-for

Package Overview
Dependencies
Maintainers
1
Versions
61
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

angular-form-for - npm Package Compare versions

Comparing version 1.0.0 to 1.2.0

dist/docs/checkbox-field.html

14

bower.json
{
"name": "angular-form-for",
"version": "1.0.0",
"version": "1.2.0",
"main": [
"./dist/form-for.css",
"./dist/form-for.js"
],
"homepage": "https://github.com/bvaughn/angular-form-for",

@@ -26,4 +30,3 @@ "authors": [

"jquery": "~2.1.1",
"lodash": "~2.4.1",
"angular": "~1.2.21"
"angular": "1.2.22"
},

@@ -33,4 +36,7 @@ "devDependencies": {

"jasmine-promise-matchers": "~0.0.3",
"angular-mocks": "~1.2.21"
"angular-mocks": "1.2.22"
},
"resolutions": {
"angular": "1.2.22"
}
}

@@ -5,2 +5,3 @@ var gulp = require('gulp');

var concat = require('gulp-concat');
var shell = require('gulp-shell');

@@ -37,5 +38,6 @@ var CONFIG = {

gulp.task('concatJs', function() {
var concatJS = function() {
var order = require('gulp-order');
var templateCache = require('gulp-angular-templatecache');
var ngAnnotate = require('gulp-ng-annotate');

@@ -57,2 +59,7 @@ var sources =

]))
.pipe(ngAnnotate());
};
gulp.task('createUncompressedJs', function() {
return concatJS()
.pipe(concat('form-for.js'))

@@ -62,2 +69,11 @@ .pipe(gulp.dest(CONFIG.distDir));

gulp.task('createCompressedJs', function() {
var uglify = require('gulp-uglify');
return concatJS()
.pipe(uglify())
.pipe(concat('form-for.min.js'))
.pipe(gulp.dest(CONFIG.distDir));
});
gulp.task('lintJs', function() {

@@ -80,2 +96,10 @@ var jshint = require('gulp-jshint');

gulp.task('build', ['clean', 'lintJs', 'test', 'concatJs', 'compileCss']);
gulp.task('docs', shell.task([
'node_modules/jsdoc/jsdoc.js ' +
'-c node_modules/angular-jsdoc/conf.json ' + // config file
'-t docs/template ' + // template file
'-d ' + CONFIG.distDir + '/docs '+ // output directory
'-r ' + CONFIG.sourceDir // source code directory
]));
gulp.task('build', ['clean', 'lintJs', 'test', 'createCompressedJs', 'createUncompressedJs', 'compileCss', 'docs']);
{
"name": "angular-form-for",
"version": "1.0.0",
"version": "1.2.0",
"description": "Set of Angular directives to simplify creating and validating HTML forms.",

@@ -36,3 +36,8 @@ "keywords": [

"ng-html2js": "^1.1.0",
"nib": "^1.0.3"
"nib": "^1.0.3",
"gulp-ng-annotate": "~0.3.0",
"gulp-uglify": "~0.3.1",
"angular-jsdoc": "~0.1.1",
"gulp-shell": "~0.2.9",
"jsdoc": "~3.3.0-alpha9"
},

@@ -39,0 +44,0 @@ "scripts": {

# Angular formFor
**Navigation**:
[Overview](https://github.com/bvaughn/angular-form-for/wiki/) |
[Input Types](https://github.com/bvaughn/angular-form-for/wiki/Input-Types) |
[Validation Types](https://github.com/bvaughn/angular-form-for/wiki/Validation-Types) |
[Templates](https://github.com/bvaughn/angular-form-for/wiki/Template-Overrides) |
[API Reference](https://github.com/bvaughn/angular-form-for/wiki/API-Reference)
![Angular formFor logo](http://bvaughn.github.io/angular-form-for/app/images/form-for-logo-small.png)
---
*formFor* is a quick and easy way to declare complex HTML forms with client and server-side validations.
Using *formFor* a sign-up form may look like this:
*formFor* is a quick and easy way to declare complex HTML forms with client and server-side validations. Using *formFor* a sign-up form may look like this:
```html
<form form-for="user" service="UserSignUp">
<text-field label="Email" attribute="email"></text-field>
<text-field label="Password" attribute="password" type="password"></text-field>
<checkbox-field label="I agree to the TOS" attribute="agreed"></checkbox-field>
<text-field attribute="email"></text-field>
<text-field attribute="password" type="password"></text-field>
<checkbox-field attribute="iAgreeToTheTerms"></checkbox-field>
<submit-button label="Sign Up"></submit-button>

@@ -23,4 +17,7 @@ </form>

But that's not all! *formFor* is incredibly flexible, offering a wide range of configuration options. Check out [the wiki](https://github.com/bvaughn/angular-form-for/wiki/) to learn more!
But that's not all! *formFor* is incredibly flexible, offering a wide range of configuration options.
Check out the website to learn more!
[http://bvaughn.github.io/angular-form-for/](http://bvaughn.github.io/angular-form-for/)
## Installation

@@ -35,4 +32,6 @@

Then just include the *formFor* module in your Angular application like so:
This will download an `angular-form-for` folder into your bower/node components directory. Inside of that folder there will be a `dist` folder with the *formFor* JavaScript and CSS files. By default *formFor* is compatible with [Bootstrap](getbootstrap.com) 3.2.x styles. A separate, *formFor* only CSS stylehseet is included for those not using Bootstrap.
Lastly just include the *formFor* module in your Angular application like so:
```js

@@ -42,16 +41,4 @@ angular.module('myAngularApp', ['formFor']);

## More information
Check out the *formFor* wiki for lots of helpful information including:
* [How to use formFor](https://github.com/bvaughn/angular-form-for/wiki/)
* [Input Types](https://github.com/bvaughn/angular-form-for/wiki/Input-Types)
* [Validation Types](https://github.com/bvaughn/angular-form-for/wiki/Validation-Types)
* [Templates](https://github.com/bvaughn/angular-form-for/wiki/Template-Overrides)
* [API Reference](https://github.com/bvaughn/angular-form-for/wiki/API-Reference)
If you have any questions please feel free to create an issue or [contact me](https://github.com/bvaughn/) directly through Github.
## License
Copyright (c) 2014 Brian Vaughn. Licensed under the MIT license.
/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#checkboxfield
* @ngdoc Directives
* @name checkbox-field
*
* @description
* Renders a checkbox &lt;input&gt; with optional label.
* This type of component is well-suited for boolean attributes.
*
* @param {String} attribute Name of the attribute within the parent form-for directive's model object.
* This attributes specifies the data-binding target for the input.
* Dot notation (ex "address.street") is supported.
* @param {Boolean} disable Disable input element.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* @param {String} help Optional help tooltip to display on hover.
* By default this makes use of the Angular Bootstrap tooltip directive and the Font Awesome icon set.
* @param {String} label Optional field label displayed after the checkbox input.
* (Although not required, it is strongly suggested that you specify a value for this attribute.) HTML is allowed for this attribute.
*
* @example
* // To display a simple TOS checkbox you might use the following markup:
* <checkbox-field label="I agree with the TOS"
* attribute="accepted">
* </checkbox-field>
*/
angular.module('formFor').directive('checkboxField',
function($log) {
function($log, FieldHelper) {
return {
require: '^formFor',
restrict: 'E',
replace: true,
restrict: 'EA',
templateUrl: 'form-for/templates/checkbox-field.html',

@@ -15,4 +34,3 @@ scope: {

disable: '@',
help: '@?',
label: '@?'
help: '@?'
},

@@ -26,3 +44,3 @@ link: function($scope, $element, $attributes, formForController) {

$scope.model = formForController.registerFormField($scope, $scope.attribute);
$scope.label = FieldHelper.getLabel($attributes, $scope.attribute);

@@ -32,8 +50,10 @@ var $input = $element.find('input');

$scope.toggle = function toggle() {
if (!$scope.disable && !$scope.disabledByForm) {
if (!$scope.disable && !$scope.model.disabled) {
$scope.model.bindable = !$scope.model.bindable;
}
};
FieldHelper.manageFieldRegistration($scope, formForController);
}
};
});
/**
* @ngdoc Directives
* @name field-label
* @description
* This component is only intended for internal use by the formFor module.
*
* @param {String} help Optional help tooltip to display on hover.
* By default this makes use of the Angular Bootstrap tooltip directive and the Font Awesome icon set.
* @param {String} label Field label string. This string can contain HTML markup.
* @param {String} required Optional attribute specifies that this field is a required field.
* If a required label has been provided via FormForConfiguration then field label will display that value for required fields.
*
* @example
* // To display a simple label with a help tooltip:
* <field-label label="Username"
* help="This will be visible to other users">
* </field-label>
*/
angular.module('formFor').directive('fieldLabel',
function( $state, $sce ) {
function( $sce, FormForConfiguration ) {
return {
restrict: 'AE',
restrict: 'EA',
templateUrl: 'form-for/templates/field-label.html',
scope: {
help: '@?',
label: '@'
label: '@',
required: '@?'
},

@@ -17,4 +33,8 @@ controller: function($scope) {

});
$scope.$watch('required', function(required) {
$scope.requiredLabel = $scope.$eval(required) ? FormForConfiguration.requiredLabel : null;
});
}
};
});
/**
* @ngdoc Directives
* @name form-for-debounce
*
* @description
* Angular introduced debouncing (via ngModelOptions) in version 1.3.
* As of the time of this writing, that version is still in beta.
* This component adds debouncing behavior for Angular 1.2.x.
* It is primarily intended for use with <input type=text> elements.
* It is primarily intended for use with &lt;input type=text&gt; and &lt;textarea&gt; elements.
*
* @param {int} formForDebounce Debounce duration in milliseconds.
* By default this value is 1000ms.
* To disable the debounce interval (aka to update on blur only) a value of false should be specified.
*
* @example
* // To configure this component to debounce with a 2 second delay:
* <input type="text"
* ng-model="username"
* form-for-debounce="2000" />
*
* // To disable the debounce interval and configure an input to update only on blur:
* <input type="text"
* ng-model="username"
* form-for-debounce="false" />
*/

@@ -30,3 +49,3 @@ angular.module('formFor').directive('formForDebounce', function($log, $timeout, FormForConfiguration) {

if (!_.isNaN(durationAttribute)) {
if (angular.isNumber(durationAttribute) && !isNaN(durationAttribute)) {
duration = durationAttribute;

@@ -33,0 +52,0 @@ }

/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#formfor
* @ngdoc Directives
* @name form-for
* @description
* This directive should be paired with an Angular ngForm object and should contain at least one of the formFor field types described below.
* At a high level, it operates on a bindable form-data object and runs validations each time a change is detected.
*
* @param {Object} controller Two way bindable attribute exposing access to the formFor controller API.
* See below for an example of how to use this binding to access the controller.
* @param {Boolean} disable Form is disabled.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* This attribute is 2-way bindable.
* @param {Object} formFor An object on $scope that formFor should read and write data to.
* To prevent accidentally persisting changes to this object after a cancelled form, it is recommended that you bind to a copied object.
* For more information refer to angular.copy.
* @param {String} service Convenience mehtod for identifying an $injector-accessible model containing both the validation rules and submit function.
* Validation rules should be accessible via an attribute named validationRules and the submit function should be named submit.
* @param {Function} submitComplete Custom handler to be invoked upon a successful form submission.
* Use this to display custom messages or do custom routing after submit.
* This method should accept a "data" parameter.
* See below for an example.
* (To set a global, default submit-with handler see FormForConfiguration.)
* @param {Function} submitError Custom error handler function.
* This function should accept an "error" parameter.
* See below for an example.
* (To set a global, default submit-with handler see FormForConfiguration.)
* @param {Function} submitWith Function triggered on form-submit.
* This function should accept a named parameter data (the model object) and should return a promise to be resolved/rejected based on the result of the submission.
* In the event of a rejection, the promise can return an error string or a map of field-names to specific errors.
* See below for an example.
* @param {Object} validationRules Set of client-side validation rules (keyed by form field names) to apply to form-data before submitting.
* For more information refer to the Validation Types page.
*/

@@ -13,3 +42,2 @@ angular.module('formFor').directive('formFor',

disable: '=?',
errorMap: '=?',
formFor: '=',

@@ -24,8 +52,30 @@ service: '@',

controller: function($scope) {
$scope.formFieldScopes = {};
$scope.bindable = {};
$scope.scopeWatcherUnwatchFunctions = [];
$scope.submitButtonScopes = [];
$scope.collectionNameToErrorMap = {};
$scope.fieldNameToErrorMap = {};
// Map of safe (bindable, $scope.$watch-able) field names to objects containing the following keys:
// • bindableWrapper: Shared between formFor and field directives. Returned by registerFormField(). Contains:
// • bindable: Used for easier 2-way data binding between formFor and input field
// • disabled: Field should be disabled (generally because form-submission is in progress)
// • error: Field should display the following validation error message
// • required: Informs the field's label if it should show a "required" marker
// • fieldName: Original field name
// • unwatchers: Array of unwatch functions to be invoked on field-unregister
// • validationAttribute: Maps field name to the location of field validation rules
//
// A note on safe field names:
// A field like 'hobbies[0].name' might be mapped to something like 'hobbies__0__name' so that we can safely $watch it.
$scope.fields = {};
// Maps collection names (ex. 'hobbies') to <collection-label> directives.
// Allows formFor to mark collections as required and to display collection-level errors.
$scope.collectionLabels = {};
// Set of bindable wrappers used to disable buttons when form-submission is in progress.
// Wrappers contain the following keys:
// • disabled: Button should be disabled (generally because form-submission is in progress)
//
// Note that there is no current way to associate a wrapper with a button.
$scope.buttons = [];
if ($scope.service) {

@@ -43,18 +93,34 @@ $scope.$service = $injector.get($scope.service);

// Attaching controller methods to a 'controller' object instead of 'this' results in prettier JSDoc display.
var controller = this;
/**
* All form-input children of formFor must register using this function.
* @param formFieldScope $scope of input directive
* @param fieldName Unique identifier of field within model; used to map errors back to input fields
* @memberof form-for
* @param {String} fieldName Unique identifier of field within model; used to map errors back to input fields
* @return {Object} Object containing keys to be observed by the input field:
* • bindable: Input should 2-way bind against this attribute in order to sync data with formFor.
* • disabled: Input should disable itself if this value becomes true; typically this means the form is being submitted.
* • error: Input should display the string contained in this field (if one exists); this means the input value is invalid.
* • required: Input should display a 'required' indicator if this value is true.
*/
this.registerFormField = function(formFieldScope, fieldName) {
var safeFieldName = NestedObjectHelper.flattenAttribute(fieldName);
controller.registerFormField = function(fieldName) {
var bindableFieldName = NestedObjectHelper.flattenAttribute(fieldName);
var rules = NestedObjectHelper.readAttribute($scope.$validationRules, fieldName);
$scope.formFieldScopes[fieldName] = formFieldScope;
$scope.bindable[safeFieldName] = {bindable: null};
// Store information about this field that we'll need for validation and binding purposes.
// @see Above documentation for $scope.fields
var fieldDatum = {
bindableWrapper: {
bindable: null,
disabled: false,
error: null,
required: ModelValidator.isFieldRequired(fieldName, $scope.$validationRules)
},
fieldName: fieldName,
unwatchers: [],
validationAttribute: fieldName.split('[')[0] // TODO Is this needed?
};
// TRICKY Why do we use $parse?
// Dot notation (ex. 'foo.bar') causes trouble with the brackets accessor.
// To simplify binding for formFor children, we encapsulate this and return a simple bindable model.
// We need to manage 2-way binding to keep the original model and our wrapper in sync though.
// Given a model {foo: {bar: 'baz'}} and a field-name 'foo.bar' $parse allows us to retrieve 'baz'.
$scope.fields[bindableFieldName] = fieldDatum;

@@ -64,34 +130,132 @@ var getter = $parse(fieldName);

$scope.$watch('bindable.' + safeFieldName + '.bindable', function(newValue, oldValue) {
if (newValue !== oldValue) {
setter($scope.formFor, newValue);
}
});
// Changes made by our field should be synced back to the form-data model.
fieldDatum.unwatchers.push(
$scope.$watch('fields.' + bindableFieldName + '.bindableWrapper.bindable', function(newValue, oldValue) {
if (newValue !== oldValue) {
setter($scope.formFor, newValue);
}
}));
$scope.$watch('formFor.' + fieldName, function(newValue, oldValue) {
$scope.bindable[safeFieldName].bindable = getter($scope.formFor);
});
var formDataWatcherInitialized;
// Also run validations on-change as necessary.
createScopeWatcher(fieldName);
// Changes made to the form-data model should likewise be synced to the field's bindable model.
// (This is necessary for data that is loaded asynchronously after a form has already been displayed.)
fieldDatum.unwatchers.push(
$scope.$watch('formFor.' + fieldName, function(newValue, oldValue) {
fieldDatum.bindableWrapper.bindable = getter($scope.formFor);
return $scope.bindable[safeFieldName];
// Changes in form-data should also trigger validations.
// Validation failures will not be displayed unless the form-field has been marked dirty (changed by user).
// We shouldn't mark our field as dirty when Angular auto-invokes the initial watcher though,
// So we ignore the first invocation...
if (!formDataWatcherInitialized) {
formDataWatcherInitialized = true;
// If formFor was binded with an empty object, ngModel will auto-initialize keys on blur.
// We shouldn't treat this as a user-edit though unless the user actually typed something.
// It's possible they typed and then erased, but that seems less likely.
// So we also shouldn't mark as dirty unless a truthy value has been provided.
} else if (oldValue !== undefined || newValue !== '') {
$scope.formForStateHelper.setFieldHasBeenModified(bindableFieldName, true);
}
// Run validations and store the result keyed by our bindableFieldName for easier subsequent lookup.
if ($scope.$validationRules) {
ModelValidator.validateField(
$scope.formFor,
fieldName,
$scope.$validationRules
).then(
function() {
$scope.formForStateHelper.setFieldError(bindableFieldName, null);
},
function(error) {
$scope.formForStateHelper.setFieldError(bindableFieldName, error);
});
}
}));
return fieldDatum.bindableWrapper;
};
/**
* Form fields created within ngRepeat or ngIf directive should clean up themselves on removal.
* @memberof form-for
* @param {String} fieldName Unique identifier of field within model; used to map errors back to input fields
*/
this.unregisterFormField = function(fieldName) {
var bindableFieldName = NestedObjectHelper.flattenAttribute(fieldName);
angular.forEach(
$scope.fields[bindableFieldName].unwatchers,
function(unwatch) {
unwatch();
});
};
/**
* All submitButton children must register with formFor using this function.
* @memberof form-for
* @param {$scope} submitButtonScope $scope of submit button directive
* @return {Object} Object containing keys to be observed by the input button:
* • disabled: Button should disable itself if this value becomes true; typically this means the form is being submitted.
*/
this.registerSubmitButton = function(submitButtonScope) {
$scope.submitButtonScopes.push(submitButtonScope);
controller.registerSubmitButton = function(submitButtonScope) {
var bindableWrapper = {
disabled: false
};
$scope.buttons.push(bindableWrapper);
return bindableWrapper;
};
/**
* Collection headers should register themselves using this function in order to be notified of validation errors.
* @memberof form-for
* @param {String} fieldName Unique identifier of collection within model
* @return {Object} Object containing keys to be observed by the input field:
* • error: Header should display the string contained in this field (if one exists); this means the collection is invalid.
* • required: Header should display a 'required' indicator if this value is true.
*/
controller.registerCollectionLabel = function(fieldName) {
var bindableFieldName = NestedObjectHelper.flattenAttribute(fieldName);
var bindableWrapper = {
error: null,
required: ModelValidator.isCollectionRequired(fieldName, $scope.$validationRules)
};
$scope.collectionLabels[bindableFieldName] = bindableWrapper;
var watcherInitialized = false;
$scope.$watch('formFor.' + fieldName + '.length', function(newValue, oldValue) {
// The initial $watch should not trigger a visible validation...
if (!watcherInitialized) {
watcherInitialized = true;
} else {
ModelValidator.validateCollection($scope.formFor, fieldName, $scope.$validationRules).then(
function() {
$scope.formForStateHelper.setFieldError(bindableFieldName, null);
},
function(error) {
$scope.formForStateHelper.setFieldError(bindableFieldName, error);
});
}
});
return bindableWrapper;
};
/**
* Resets errors displayed on the <form> without resetting the form data values.
* @memberof form-for
*/
this.resetErrors = function() {
controller.resetErrors = function() {
$scope.formForStateHelper.setFormSubmitted(false);
var keys = NestedObjectHelper.flattenObjectKeys($scope.errorMap);
var keys = NestedObjectHelper.flattenObjectKeys($scope.fieldNameToErrorMap);
_.each(keys, function(fieldName) {
angular.forEach(keys, function(fieldName) {
$scope.formForStateHelper.setFieldHasBeenModified(fieldName, false);

@@ -103,14 +267,13 @@ });

$scope.controller = $scope.controller || {};
$scope.controller.registerFormField = this.registerFormField;
$scope.controller.registerSubmitButton = this.registerSubmitButton;
$scope.controller.resetErrors = this.resetErrors;
angular.copy(controller, $scope.controller);
// Disable all child inputs if the form becomes disabled.
$scope.$watch('disable', function(value) {
_.each($scope.formFieldScopes, function(scope) {
scope.disabledByForm = value;
angular.forEach($scope.fields, function(field) {
field.bindableWrapper.disabled = value;
});
_.each($scope.submitButtonScopes, function(scope) {
scope.disabledByForm = value;
angular.forEach($scope.buttons, function(wrapper) {
wrapper.disabled = value;
});

@@ -122,65 +285,54 @@ });

/**
* Setup a debounce validator on a registered form field.
* This validator will update error messages inline as the user progresses through the form.
*/
var createScopeWatcher = function(fieldName) {
var formFieldScope = $scope.formFieldScopes[fieldName];
var initialized;
return $scope.$watch('formFor.' + fieldName,
function(newValue, oldValue) {
// Scope watchers always trigger once when added.
// Only mark our field dirty the second time this watch is triggered.
if (initialized) {
$scope.formForStateHelper.setFieldHasBeenModified(fieldName, true);
}
initialized = true;
if ($scope.$validationRules) {
ModelValidator.validateField($scope.formFor, fieldName, $scope.$validationRules).then(
function() {
$scope.formForStateHelper.setFieldError(fieldName, null);
},
function(error) {
$scope.formForStateHelper.setFieldError(fieldName, error);
});
}
});
};
// Watch for any validation changes or changes in form-state that require us to notify the user.
// Rather than using a deep-watch, FormForStateHelper exposes a bindable attribute 'watchable'.
// This attribute is gauranteed to change whenever validation criteria change, but its value is meaningless.
// This attribute is gauranteed to change whenever validation criteria change (but its value is meaningless).
$scope.$watch('formForStateHelper.watchable', function() {
var formForStateHelper = $scope.formForStateHelper;
var hasFormBeenSubmitted = $scope.formForStateHelper.hasFormBeenSubmitted();
_.each($scope.formFieldScopes, function(scope, fieldName) {
if (formForStateHelper.hasFormBeenSubmitted() || formForStateHelper.hasFieldBeenModified(fieldName)) {
var error = formForStateHelper.getFieldError(fieldName);
// Mark invalid fields
angular.forEach($scope.fields, function(fieldDatum, bindableFieldName) {
if (hasFormBeenSubmitted || $scope.formForStateHelper.hasFieldBeenModified(bindableFieldName)) {
var error = $scope.formForStateHelper.getFieldError(bindableFieldName);
scope.error = error ? $sce.trustAsHtml(error) : null;
fieldDatum.bindableWrapper.error = error ? $sce.trustAsHtml(error) : null;
} else {
// Clear out field errors in the event that the form has been reset.
scope.error = null;
fieldDatum.bindableWrapper.error = null; // Clear out field errors in the event that the form has been reset.
}
});
// Mark invalid collections
angular.forEach($scope.collectionLabels, function(bindableWrapper, bindableFieldName) {
var error = $scope.formForStateHelper.getFieldError(bindableFieldName);
bindableWrapper.error = error ? $sce.trustAsHtml(error) : null;
});
});
/**
/*
* Update all registered collection labels with the specified error messages.
* Specified map should be keyed with fieldName and should container user-friendly error strings.
* @param {Object} fieldNameToErrorMap Map of collection names (or paths) to errors
*/
$scope.updateCollectionErrors = function(fieldNameToErrorMap) {
angular.forEach($scope.collectionLabels, function(bindableWrapper, bindableFieldName) {
var error = NestedObjectHelper.readAttribute(fieldNameToErrorMap, bindableFieldName);
$scope.formForStateHelper.setFieldError(bindableFieldName, error);
});
};
/*
* Update all registered form fields with the specified error messages.
* Specified map should be keyed with fieldName and should container user-friendly error strings.
*
* @param errorMap Map of field names (or paths) to errors
* @param {Object} fieldNameToErrorMap Map of field names (or paths) to errors
*/
$scope.updateErrors = function(errorMap) {
_.each($scope.formFieldScopes, function(scope, fieldName) {
var error = NestedObjectHelper.readAttribute(errorMap, fieldName);
$scope.updateFieldErrors = function(fieldNameToErrorMap) {
angular.forEach($scope.fields, function(scope, bindableFieldName) {
var error = NestedObjectHelper.readAttribute(fieldNameToErrorMap, scope.fieldName);
$scope.formForStateHelper.setFieldError(fieldName, error);
$scope.formForStateHelper.setFieldError(bindableFieldName, error);
});
};
/**
/*
* Validate all registered fields and update FormForStateHelper's error mapping.

@@ -190,31 +342,39 @@ * This update indirectly triggers form validity check and inline error message display.

$scope.validateAll = function() {
$scope.updateErrors({});
var validationPromise;
// Reset errors before starting new validation.
$scope.updateCollectionErrors({});
$scope.updateFieldErrors({});
var validateCollectionsPromise;
var validateFieldsPromise;
if ($scope.$validationRules) {
validationPromise =
ModelValidator.validateFields(
$scope.formFor,
_.keys($scope.formFieldScopes),
$scope.$validationRules);
var validationKeys = [];
angular.forEach($scope.fields, function(field) {
validationKeys.push(field.fieldName);
});
validateFieldsPromise = ModelValidator.validateFields($scope.formFor, validationKeys, $scope.$validationRules);
validateFieldsPromise.then(angular.noop, $scope.updateFieldErrors);
validationKeys = [];
angular.forEach($scope.collectionLabels, function(bindableWrapper, bindableFieldName) {
validationKeys.push(bindableFieldName);
});
validateCollectionsPromise = ModelValidator.validateFields($scope.formFor, validationKeys, $scope.$validationRules);
validateCollectionsPromise.then(angular.noop, $scope.updateCollectionErrors);
} else {
validationPromise = $q.resolve();
validateCollectionsPromise = $q.resolve();
validateFieldsPromise = $q.resolve();
}
validationPromise.then(angular.noop, $scope.updateErrors);
return validationPromise;
return $q.waitForAll([validateCollectionsPromise, validateFieldsPromise]);
};
// Clean up dangling watchers on destroy.
$scope.$on('$destroy', function() {
_.each($scope.scopeWatcherUnwatchFunctions, function(unwatch) {
unwatch();
});
});
},
link: function($scope, $element, $attributes, controller) {
// Override form submit to trigger overall validation.
$element.submit(
$element.on('submit', // Override form submit to trigger overall validation.
function() {

@@ -234,5 +394,10 @@ $scope.formForStateHelper.setFormSubmitted(true);

} else {
promise = $q.reject('No submit implementation provided');
promise = $q.reject('No submit function provided');
}
// Issue #18 Guard against submit functions that don't return a promise by warning rather than erroring.
if (!promise) {
promise = $q.reject('Submit function did not return a promise');
}
promise.then(

@@ -250,4 +415,6 @@ function(response) {

// This is unecessary if a string was returned.
if (_.isObject(errorMessageOrErrorMap)) {
$scope.updateErrors(errorMessageOrErrorMap);
if (angular.isObject(errorMessageOrErrorMap)) {
// TODO Questionable: Maybe server should be forced to return fields/collections constraints?
$scope.updateCollectionErrors(errorMessageOrErrorMap);
$scope.updateFieldErrors(errorMessageOrErrorMap);
}

@@ -254,0 +421,0 @@

/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#radiofield
* @ngdoc Directives
* @name radio-field
*
* @description
* Renders a radio &lt;input&gt; with optional label.
* This type of component is well-suited for small enumerations.
*
* @param {String} attribute Name of the attribute within the parent form-for directive's model object.
* This attributes specifies the data-binding target for the input.
* Dot notation (ex "address.street") is supported.
* @param {Boolean} disable Disable input element.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* @param {String} help Optional help tooltip to display on hover.
* By default this makes use of the Angular Bootstrap tooltip directive and the Font Awesome icon set.
* @param {String} label Optional field label displayed after the radio input.
* (Although not required, it is strongly suggested that you specify a value for this attribute.)
* HTML is allowed for this attribute
* @param {Object} Value to be assigned to model if this radio component is selected.
*
* @example
* // To render a radio group for gender selection you might use the following markup:
* <radio-field label="Female" attribute="gender" value="f"></radio-field>
* <radio-field label="Male" attribute="gender" value="m"></radio-field>
*/
angular.module('formFor').directive('radioField',
function($log) {
function($log, FieldHelper) {
var nameToActiveRadioMap = {};

@@ -11,4 +32,3 @@

require: '^formFor',
restrict: 'E',
replace: true,
restrict: 'EA',
templateUrl: 'form-for/templates/radio-field.html',

@@ -19,3 +39,2 @@ scope: {

help: '@?',
label: '@?',
value: '@'

@@ -31,7 +50,10 @@ },

if (!nameToActiveRadioMap[$scope.attribute]) {
nameToActiveRadioMap[$scope.attribute] = {
var mainRadioDatum = {
defaultScope: $scope,
scopes: [],
model: formForController.registerFormField($scope, $scope.attribute)
scopes: []
};
FieldHelper.manageFieldRegistration($scope, formForController);
nameToActiveRadioMap[$scope.attribute] = mainRadioDatum;
}

@@ -45,3 +67,3 @@

$scope.model = activeRadio.model;
$scope.label = FieldHelper.getLabel($attributes, $scope.value);

@@ -51,3 +73,3 @@ var $input = $element.find('input');

$scope.click = function() {
if (!$scope.disable && !$scope.disabledByForm) {
if (!$scope.disable && !$scope.model.disabled) {
$scope.model.bindable = $scope.value;

@@ -57,7 +79,12 @@ }

activeRadio.defaultScope.$watch('model', function(value) {
$scope.model = value;
});
activeRadio.defaultScope.$watch('disable', function(value) {
$scope.disable = value;
});
activeRadio.defaultScope.$watch('disabledByForm', function(value) {
$scope.disabledByForm = value;
activeRadio.defaultScope.$watch('model.disabled', function(value) {
if ($scope.model) {
$scope.model.disabled = value;
}
});

@@ -64,0 +91,0 @@

/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#selectfield
* @ngdoc Directives
* @name select-field
* @description
* Renders a drop-down &lt;select&gt; menu along with an input label.
* This type of component works with large enumerations and can be configured to allow for a blank/empty selection by way of an allow-blank attribute.
*
* @param {attribute} allow-blank The presence of this attribute indicates that an empty/blank selection should be allowed.
* @param {String} attribute Name of the attribute within the parent form-for directive's model object.
* This attributes specifies the data-binding target for the input.
* Dot notation (ex "address.street") is supported.
* @param {Boolean} disable Disable input element.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* @param {Boolean} enableFiltering Enable filtering of list via a text input at the top of the dropdown.
* @param {String} filter Two-way bindable filter string.
* $watch this property to load remote options based on filter text.
* (Refer to this Plunker demo for an example.)
* @param {String} help Optional help tooltip to display on hover.
* By default this makes use of the Angular Bootstrap tooltip directive and the Font Awesome icon set.
* @param {String} label Optional field label displayed before the drop-down.
* (Although not required, it is strongly suggested that you specify a value for this attribute.) HTML is allowed for this attribute.
* @param {String} labelAttribute Optional override for label key in options array.
* Defaults to "label".
* @param {Array} options Set of options, each containing a label and value key.
* The label is displayed to the user and the value is assigned to the corresponding model attribute on selection.
* @param {String} placeholder Optional placeholder text to display if no value has been selected.
* The text "Select" will be displayed if no placeholder is provided.
* @param {String} valueAttribute Optional override for value key in options array.
* Defaults to "value".
*
* @example
* // To use this component you'll first need to define a set of options. For instance:
* $scope.genders = [
* { value: 'f', label: 'Female' },
* { value: 'm', label: 'Male' }
* ];
*
* // To render a drop-down input using the above options:
* <select-field attribute="gender"
* label="Gender"
* options="genders">
* </select-field>
*
* // If you want to make this attribute optional you can use the allow-blank attribute as follows:
* <select-field attribute="gender"
* label="Gender"
* options="genders"
* allow-blank>
* </select-field>
*/
angular.module('formFor').directive('selectField',
function($log, $timeout) {
function($document, $log, $timeout, FieldHelper) {
return {
require: '^formFor',
restrict: 'E',
replace: true,
restrict: 'EA',
templateUrl: 'form-for/templates/select-field.html',

@@ -15,4 +60,5 @@ scope: {

disable: '@',
filter: '=?',
filterDebounce: '@?',
help: '@?',
label: '@?',
options: '=',

@@ -22,4 +68,4 @@ placeholder: '@?'

link: function($scope, $element, $attributes, formForController) {
if (!$scope.attribute || !$scope.options) {
$log.error('Missing required field(s) "attribute" or "options"');
if (!$scope.attribute) {
$log.error('Missing required field "attribute"');

@@ -30,14 +76,94 @@ return;

$scope.allowBlank = $attributes.hasOwnProperty('allowBlank');
$scope.model = formForController.registerFormField($scope, $scope.attribute);
$scope.enableFiltering = $attributes.hasOwnProperty('enableFiltering');
// TODO Track scroll position and viewport height and expand upward if needed
$scope.labelAttribute = $attributes.labelAttribute || 'label';
$scope.valueAttribute = $attributes.valueAttribute || 'value';
$scope.$watch('model.bindable', function(value) {
var option = _.find($scope.options,
$scope.label = FieldHelper.getLabel($attributes, $scope.attribute);
FieldHelper.manageFieldRegistration($scope, formForController);
/*****************************************************************************************
* The following code pertains to filtering visible options.
*****************************************************************************************/
$scope.emptyOption = {};
$scope.filteredOptions = [];
var sanitize = function(value) {
return value && value.toLowerCase();
};
var calculateFilteredOptions = function() {
var options = $scope.options || [];
$scope.filteredOptions.splice(0);
if (!$scope.enableFiltering || !$scope.filter) {
angular.copy(options, $scope.filteredOptions);
} else {
var filter = sanitize($scope.filter);
angular.forEach(options, function(option) {
var index = sanitize(option[$scope.labelAttribute]).indexOf(filter);
if (index >= 0) {
$scope.filteredOptions.push(option);
}
});
}
if ($scope.allowBlank) {
$scope.filteredOptions.unshift($scope.emptyOption);
}
};
$scope.$watch('filter', calculateFilteredOptions);
$scope.$watch('options.length', calculateFilteredOptions);
/*****************************************************************************************
* The following code manages setting the correct default value based on bindable model.
*****************************************************************************************/
var updateDefaultOption = function() {
var selected = $scope.selectedOption && $scope.selectedOption[[$scope.valueAttribute]];
var matchingOption;
if ($scope.model.bindable === selected) {
return;
}
angular.forEach($scope.options,
function(option) {
return value === option.value;
if (option[$scope.valueAttribute] === $scope.model.bindable) {
matchingOption = option;
}
});
$scope.selectedOption = option;
$scope.selectedOptionLabel = option && option.label;
$scope.selectedOption = matchingOption;
$scope.selectedOptionLabel = matchingOption && matchingOption[$scope.labelAttribute];
};
$scope.$watch('model.bindable', updateDefaultOption);
$scope.$watch('options', updateDefaultOption);
/*****************************************************************************************
* The following code deals with toggling/collapsing the drop-down and selecting values.
*****************************************************************************************/
$scope.$watch('model.bindable', function(value) {
var matchingOption;
for (var index = 0; index < $scope.filteredOptions.length; index++) {
var option = $scope.filteredOptions[index];
if (option[$scope.valueAttribute] === value) {
matchingOption = option;
break;
}
};
$scope.selectedOption = matchingOption;
$scope.selectedOptionLabel = matchingOption && matchingOption[$scope.labelAttribute];
});

@@ -49,11 +175,19 @@

}, 1);
}
};
var removeClickWatch = function() {
$document.off('click', clickWatcher);
};
var addClickToOpen = function() {
oneClick($element.find('.select-field-toggle-button'), clickToOpen);
};
$scope.selectOption = function(option) {
$scope.model.bindable = option && option.value;
$scope.model.bindable = option && option[$scope.valueAttribute];
$scope.isOpen = false;
$(document).off('click', clickWatcher);
removeClickWatch();
oneClick($element, clickToOpen);
addClickToOpen();
};

@@ -65,10 +199,14 @@

oneClick($element, clickToOpen);
removeClickWatch();
addClickToOpen();
};
var scroller = $element.find('.select-field-dropdown-list-container');
var list = $element.find('ul');
var scroller = $element.find('.list-group-container');
var list = $element.find('.list-group');
var clickToOpen = function() {
if ($scope.disable || $scope.disabledByForm) {
if ($scope.disable || $scope.model.disabled) {
addClickToOpen();
return;

@@ -80,27 +218,75 @@ }

if ($scope.isOpen) {
oneClick($(document), clickWatcher);
// TODO Determine whether to open downward or upward
// TODO Auto-focus input field if filterable
oneClick($document, clickWatcher);
var value = $scope.model.bindable;
$timeout(function() {
var listItem =
_.find(list.find('li'),
function(listItem) {
$timeout(
angular.bind(
this,
function() {
var listItems = list.find('.list-group-item');
var matchingListItem;
for (var index = 0; index < listItems.length; index++) {
var listItem = listItems[index];
var option = $(listItem).scope().option;
return option && option.value === value;
});
if (option && option[$scope.valueAttribute] === value) {
matchingListItem = listItem;
if (listItem) {
scroller.scrollTop(
$(listItem).offset().top - $(listItem).parent().offset().top);
}
}.bind(this), 1);
break;
}
}
if (matchingListItem) {
scroller.scrollTop(
$(matchingListItem).offset().top - $(matchingListItem).parent().offset().top);
}
}), 1);
}
};
oneClick($element, clickToOpen);
addClickToOpen();
/*****************************************************************************************
* The following code responds to keyboard events when the drop-down is visible
*****************************************************************************************/
$scope.mouseOver = function(index) {
$scope.mouseOverIndex = index;
$scope.mouseOverOption = index >= 0 ? $scope.filteredOptions[index] : null;
};
// Listen to key down, not up, because ENTER key sometimes gets converted into a click event.
$scope.keyDown = function(event) {
switch (event.keyCode) {
case 27: // Escape key
$scope.isOpen = false;
break;
case 13: // Enter key
$scope.selectOption($scope.mouseOverOption);
$scope.isOpen = false;
// Don't bubble up and submit the parent form
event.preventDefault();
event.stopPropagation();
break;
case 38: // Up arrow
$scope.mouseOver( $scope.mouseOverIndex > 0 ? $scope.mouseOverIndex - 1 : $scope.filteredOptions.length - 1 );
break;
case 40: // Down arrow
$scope.mouseOver( $scope.mouseOverIndex < $scope.filteredOptions.length - 1 ? $scope.mouseOverIndex + 1 : 0 );
break;
}
};
$scope.$watchCollection('[isOpen, filteredOptions.length]', function() {
$scope.mouseOver(-1); // Reset hover anytime our list opens/closes or our collection is refreshed.
});
$scope.$on('$destroy', function() {
$(document).off('click', clickWatcher);
removeClickWatch();
});

@@ -107,0 +293,0 @@ }

/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#submitbutton
* @ngdoc Directives
* @name submit-button
*
* @description
* Displays a submit &lt;button&gt; component that is automatically disabled when a form is invalid or in the process of submitting.
*
* @param {String} class Optional CSS class names to apply to button component.
* @param {Boolean} disable Disable button.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* @param {String} icon Optional CSS class to display as a button icon.
* @param {String} label Button label.
* HTML is allowed for this attribute.
*
* @example
* // Here is a simple submit button with an icon:
* <submit-button label="Sign Up" icon="fa fa-user"></submit-button>
*
* // You can use your own <button> components within a formFor as well.
* // If you choose to, it is recommended that you bind your buttons disabled attribute to a disabledByForm scope property (managed by formFor) as follows:
* <form form-for="formData">
* <button ng-disabled="disabledByForm">Submit</button>
* </form>
*/

@@ -9,4 +29,3 @@ angular.module('formFor').directive('submitButton',

require: '^formFor',
replace: true,
restrict: 'E',
restrict: 'EA',
templateUrl: 'form-for/templates/submit-button.html',

@@ -13,0 +32,0 @@ scope: {

/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#textfield
* @ngdoc Directives
* @name text-field
* @description
* Displays a HTML &lt;input&gt; element along with an input label.
* This directive can be configured to optionally display an informational tooltip.
* In the event of a validation error, this directive will also render an inline error message.
*
* @param {String} attribute Name of the attribute within the parent form-for directive's model object.
* This attributes specifies the data-binding target for the input.
* Dot notation (ex "address.street") is supported.
* @param {attribute} autofocus The presence of this attribute will auto-focus the input field.
* @param {int} debounce Debounce duration (in ms) before input text is applied to model and evaluated.
* To disable debounce (update only on blur) specify a value of false.
* This value's default is determined by FormForConfiguration.
* @param {Boolean} disable Disable input element.
* (Note the name is disable and not disabled to avoid collisions with the HTML5 disabled attribute).
* @param {String} help Optional help tooltip to display on hover.
* By default this makes use of the Angular Bootstrap tooltip directive and the Font Awesome icon set.
* @param {Function} focused Optional function to be invoked on text input focus.
* @param {String} iconAfter Optional CSS class to display as an icon after the input field.
* @param {Function} iconAfterClicked Optional function to be invoked when the after-icon is clicked.
* @param {String} iconBefore Optional CSS class to display as a icon before the input field.
* @param {Function} iconBeforeClicked Optional function to be invoked when the before-icon is clicked.
* @param {String} label Optional field label displayed before the input.
* (Although not required, it is strongly suggested that you specify a value for this attribute.) HTML is allowed for this attribute.
* @param {attribute} multiline The presence of this attribute enables multi-line input.
* @param {String} placeholder Optional placeholder text to display if input is empty.
* @param {String} type Optional HTML input-type (ex.
* text, password, etc.).
* Defaults to "text".
*
* @example
* // To create a password input you might use the following markup:
* <text-field attribute="password" label="Password" type="password"></text-field>
*
* // To create a more advanced input field, with placeholder text and help tooltip you might use the following markup:
* <text-field attribute="username" label="Username"
* placeholder="Example brianvaughn"
* help="Your username will be visible to others!"></text-field>
*
* // To render a multiline text input (or <textarea>):
* <text-field attribute="description" label="Description" multiline></text-field>
*/
angular.module('formFor').directive('textField',
function($log) {
function($log, $timeout, FieldHelper) {
return {
require: '^formFor',
restrict: 'E',
replace: true,
restrict: 'EA',
templateUrl: 'form-for/templates/text-field.html',

@@ -16,5 +55,8 @@ scope: {

disable: '@',
focused: '&?',
help: '@?',
icon: '@?',
label: '@?',
iconAfter: '@?',
iconAfterClicked: '&?',
iconBefore: '@?',
iconBeforeClicked: '&?',
placeholder: '@?'

@@ -29,8 +71,31 @@ },

$scope.label = FieldHelper.getLabel($attributes, $scope.attribute);
$scope.type = $attributes.type || 'text';
$scope.multiline = $attributes.hasOwnProperty('multiline') && $attributes.multiline !== 'false';
$scope.model = formForController.registerFormField($scope, $scope.attribute);
if ($attributes.hasOwnProperty('autofocus')) {
$timeout(function() {
$element.find( $scope.multiline ? 'textarea' : 'input' ).focus();
});
}
$scope.onIconAfterClick = function() {
if ($attributes.hasOwnProperty('iconAfterClicked')) {
$scope.iconAfterClicked();
}
};
$scope.onIconBeforeClick = function() {
if ($attributes.hasOwnProperty('iconBeforeClicked')) {
$scope.iconBeforeClicked();
}
};
$scope.onFocus = function() {
if ($attributes.hasOwnProperty('focused')) {
$scope.focused();
}
};
FieldHelper.manageFieldRegistration($scope, formForController);
}
};
});
/**
* For documentation please refer to the project wiki:
* https://github.com/bvaughn/angular-form-for/wiki/API-Reference#formfor
* @ngdoc Services
* @name FormForConfiguration
* @description
* This service can be used to configure default behavior for all instances of formFor within a project.
* Note that it is a service accessible to during the run loop and not a provider accessible during config.
*/

@@ -8,15 +11,203 @@ angular.module('formFor').service('FormForConfiguration',

return {
autoGenerateLabels: false,
defaultDebounceDuration: 1000,
defaultSubmitComplete: angular.noop,
defaultSubmitError: angular.noop,
requiredLabel: null,
validationFailedForCustomMessage: 'Failed custom validation',
validationFailedForPatternMessage: 'Invalid format',
validationFailedForMaxCollectionSizeMessage: 'Must be fewer than {{num}} items',
validationFailedForMaxLengthMessage: 'Must be fewer than {{num}} characters',
validationFailedForMinCollectionSizeMessage: 'Must at least {{num}} items',
validationFailedForMinLengthMessage: 'Must be at least {{num}} characters',
validationFailedForRequiredMessage: 'Required field',
validationFailedForEmailTypeMessage: 'Invalid email format',
validationFailedForIntegerTypeMessage: 'Must be an integer',
validationFailedForNegativeTypeMessage: 'Must be negative',
validationFailedForNumericTypeMessage: 'Must be numeric',
validationFailedForPositiveTypeMessage: 'Must be positive',
/**
* Use this method to disable auto-generated labels for formFor input fields.
* @memberof FormForConfiguration
*/
disableAutoLabels: function() {
this.autoGenerateLabels = false;
},
/**
* Use this method to enable auto-generated labels for formFor input fields.
* Labels will be generated based on attribute-name for fields without a label attribute present.
* Radio fields are an exception to this rule.
* Their names are generated from their values.
* @memberof FormForConfiguration
*/
enableAutoLabels: function() {
this.autoGenerateLabels = true;
},
/**
* Sets the default debounce interval for all textField inputs.
* This setting can be overridden on a per-input basis (see textField).
* @memberof FormForConfiguration
* @param {int} duration Debounce duration (in ms).
* Defaults to 1000ms.
* To disable debounce (update only on blur) pass false.
*/
setDefaultDebounceDuration: function(value) {
this.defaultDebounceDuration = value;
},
/**
* Sets the default submit-complete behavior for all formFor directives.
* This setting can be overridden on a per-form basis (see formFor).
* @memberof FormForConfiguration
* @param {Function} method Default handler function accepting a data parameter representing the server-response returned by the submitted form.
* This function should accept a single parameter, the response data from the form-submit method.
*/
setDefaultSubmitComplete: function(value) {
this.defaultSubmitComplete = value;
},
/**
* Sets the default submit-error behavior for all formFor directives.
* This setting can be overridden on a per-form basis (see formFor).
* @memberof FormForConfiguration
* @param {Function} method Default handler function accepting an error parameter representing the data passed to the rejected submit promise.
* This function should accept a single parameter, the error returned by the form-submit method.
*/
setDefaultSubmitError: function(value) {
this.defaultSubmitError = value;
},
/**
* Sets a default label to be displayed beside each text and select input for required attributes only.
* @memberof FormForConfiguration
* @param {String} value Message to be displayed next to the field label (ex. "*", "required")
*/
setRequiredLabel: function(value) {
this.requiredLabel = value;
},
/**
* Override the default error message for failed custom validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForCustomMessage: function(value) {
this.validationFailedForCustomMessage = value;
},
/**
* Override the default error message for failed max collection size validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForMaxCollectionSizeMessage: function(value) {
this.validationFailedForMaxCollectionSizeMessage = value;
},
/**
* Override the default error message for failed maxlength validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForMaxLengthMessage: function(value) {
this.validationFailedForMaxLengthMessage = value;
},
/**
* Override the default error message for failed min collection size validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForMinCollectionSizeMessage: function(value) {
this.validationFailedForMaxCollectionSizeMessage = value;
},
/**
* Override the default error message for failed minlength validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForMinLengthMessage: function(value) {
this.validationFailedForMinLengthMessage = value;
},
/**
* Override the default error message for failed pattern validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForPatternMessage: function(value) {
this.validationFailedForPatternMessage = value;
},
/**
* Override the default error message for failed required validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForRequiredMessage: function(value) {
this.validationFailedForRequiredMessage = value;
},
/**
* Override the default error message for failed type = 'email' validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForEmailTypeMessage: function(value) {
this.validationFailedForEmailTypeMessage = value;
},
/**
* Override the default error message for failed type = 'integer' validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForIntegerTypeMessage: function(value) {
this.validationFailedForIntegerTypeMessage = value;
},
/**
* Override the default error message for failed type = 'negative' validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForNegativeTypeMessage: function(value) {
this.validationFailedForNegativeTypeMessage = value;
},
/**
* Override the default error message for failed type = 'numeric' validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForNumericTypeMessage: function(value) {
this.validationFailedForNumericTypeMessage = value;
},
/**
* Override the default error message for failed type = 'positive' validations.
* This setting applies to all instances of formFor unless otherwise overridden on a per-rule basis.
* @memberof FormForConfiguration
* @param {String} value Custom error message string
*/
setValidationFailedForPositiveTypeMessage: function(value) {
this.validationFailedForPositiveTypeMessage = value;
}
};
});

@@ -1,2 +0,2 @@

/**
/*
* Organizes state management for form-submission and field validity.

@@ -39,3 +39,7 @@ * Intended for use only by formFor directive.

FormForStateHelper.prototype.isFormValid = function() {
return _.isEmpty(this.shallowErrorMap);
for (var prop in this.shallowErrorMap) {
return false;
}
return true;
};

@@ -42,0 +46,0 @@

/**
* @ngdoc Services
* @name ModelValidator
* @description
* ModelValidator service used by formFor to determine if each field in the form-data passes validation rules.
* This service is not intended for use outside of the formFor module/library.
*/
angular.module('formFor').service('ModelValidator', function($parse, $q, NestedObjectHelper) {
angular.module('formFor').service('ModelValidator',
function($interpolate, $q, FormForConfiguration, NestedObjectHelper) {
/**
* Validates the model against all rules in the validationRules.
* This method returns a promise to be resolved on successful validation,
* Or rejected with a map of field-name to error-message.
*
* @param model Model data to validate with any existing rules
* @param validationRules Set of named validation rules
*/
this.validateAll = function(model, validationRules) {
var fields = NestedObjectHelper.flattenObjectKeys(validationRules);
/*
* Strip array brackets from field names so that model values can be mapped to rules.
* For instance:
* • 'foo[0].bar' should be validated against 'foo.collection.fields.bar'.
*/
this.$getRulesForFieldName = function(validationRules, fieldName) {
fieldName = fieldName.replace(/\[[^\]]+\]/g, '.collection.fields');
return this.validateFields(model, fields, validationRules);
};
return NestedObjectHelper.readAttribute(validationRules, fieldName);
};
/**
* Validates the values in model with the rules defined in the current validationRules.
* This method returns a promise to be resolved on successful validation,
* Or rejected with a map of field-name to error-message.
*
* @param model Model data
* @param fieldNames Whitelist set of fields to validate for the given model; values outside of this list will be ignored
* @param validationRules Set of named validation rules
*/
this.validateFields = function(model, fieldNames, validationRules) {
var deferred = $q.defer();
var promises = [];
var errorMap = {};
var that = this;
/**
* Convenience method for determining if the specified collection is flagged as required (aka min length).
*/
this.isCollectionRequired = function(fieldName, validationRules) {
var rules = this.$getRulesForFieldName(validationRules, fieldName);
_.each(fieldNames, function(fieldName) {
var rules = NestedObjectHelper.readAttribute(validationRules, fieldName);
return rules &&
rules.collection &&
rules.collection.min &&
(angular.isObject(rules.collection.min) ? rules.collection.min.rule : rules.collection.min);
};
if (rules) {
var promise = that.validateField(model, fieldName, validationRules);
/**
* Convenience method for determining if the specified field is flagged as required.
*/
this.isFieldRequired = function(fieldName, validationRules) {
var rules = this.$getRulesForFieldName(validationRules, fieldName);
promise.then(
angular.noop,
function(error) {
$parse(fieldName).assign(errorMap, error);
});
return rules &&
rules.required &&
(angular.isObject(rules.required) ? rules.required.rule : rules.required);
};
promises.push(promise);
}
});
/**
* Validates the model against all rules in the validationRules.
* This method returns a promise to be resolved on successful validation,
* Or rejected with a map of field-name to error-message.
* @memberof ModelValidator
* @param {Object} model Form-data object model is contained within
* @param {Object} validationRules Set of named validation rules
* @returns {Promise} To be resolved or rejected based on validation success or failure.
*/
this.validateAll = function(model, validationRules) {
var fields = NestedObjectHelper.flattenObjectKeys(validationRules);
$q.waitForAll(promises).then(
deferred.resolve,
function() {
deferred.reject(errorMap);
});
return this.validateFields(model, fields, validationRules);
};
return deferred.promise;
};
/**
* Validates the values in model with the rules defined in the current validationRules.
* This method returns a promise to be resolved on successful validation,
* Or rejected with a map of field-name to error-message.
* @memberof ModelValidator
* @param {Object} model Form-data object model is contained within
* @param {Array} fieldNames Whitelist set of fields to validate for the given model; values outside of this list will be ignored
* @param {Object} validationRules Set of named validation rules
* @returns {Promise} To be resolved or rejected based on validation success or failure.
*/
this.validateFields = function(model, fieldNames, validationRules) {
var deferred = $q.defer();
var promises = [];
var errorMap = {};
/**
* Validates a value against the related rule-set (within validationRules).
* This method returns a promise to be resolved on successful validation.
* If validation fails the promise will be rejected with an error message.
*
* @param model Form-data object model is contained within
* @param fieldName Name of field used to associate the rule-set map with a given value
* @param validationRules Set of named validation rules
*/
this.validateField = function(model, fieldName, validationRules) {
var rules = NestedObjectHelper.readAttribute(validationRules, fieldName);
var value = $parse(fieldName)(model);
angular.forEach(fieldNames, function(fieldName) {
var rules = this.$getRulesForFieldName(validationRules, fieldName);
if (rules) {
value = value || '';
if (rules) {
var promise;
if (rules.required) {
var required = _.isObject(rules.required) ? rules.required.rule : rules.required;
if (rules.collection) {
promise = this.validateCollection(model, fieldName, validationRules);
} else {
promise = this.validateField(model, fieldName, validationRules);
}
if (!!value !== required) {
var errorMessage = _.isObject(rules.required) ? rules.required.message : 'Required field';
promise.then(
angular.noop,
function(error) {
NestedObjectHelper.writeAttribute(errorMap, fieldName, error);
});
return $q.reject(errorMessage);
promises.push(promise);
}
}
}, this);
if (rules.minlength) {
var minlength = _.isObject(rules.minlength) ? rules.minlength.rule : rules.minlength;
$q.waitForAll(promises).then(
deferred.resolve,
function() {
deferred.reject(errorMap);
});
if (value.length < minlength) {
var errorMessage = _.isObject(rules.minlength) ? rules.minlength.message : 'Must be at least ' + minlength + ' characters';
return deferred.promise;
};
return $q.reject(errorMessage);
/**
* Validate the properties of a collection (but not the items within the collection).
* This method returns a promise to be resolved on successful validation,
* Or rejected with an error message.
* @memberof ModelValidator
* @param {Object} model Form-data object model is contained within
* @param {Array} fieldName Name of collection to validate
* @param {Object} validationRules Set of named validation rules
* @returns {Promise} To be resolved or rejected based on validation success or failure.
*/
this.validateCollection = function(model, fieldName, validationRules) {
var rules = this.$getRulesForFieldName(validationRules, fieldName);
var collection = NestedObjectHelper.readAttribute(model, fieldName);
if (rules && rules.collection) {
collection = collection || [];
var collectionRules = rules.collection;
if (collectionRules.min) {
var min = angular.isObject(collectionRules.min) ? collectionRules.min.rule : collectionRules.min;
if (collection.length < min) {
return $q.reject(
angular.isObject(collectionRules.min) ?
collectionRules.min.message :
$interpolate(FormForConfiguration.validationFailedForMinCollectionSizeMessage)({num: min}));
}
}
if (collectionRules.max) {
var max = angular.isObject(collectionRules.max) ? collectionRules.max.rule : collectionRules.max;
if (collection.length > max) {
return $q.reject(
angular.isObject(collectionRules.max) ?
collectionRules.max.message :
$interpolate(FormForConfiguration.validationFailedForMaxCollectionSizeMessage)({num: max}));
}
}
}
if (rules.maxlength) {
var maxlength = _.isObject(rules.maxlength) ? rules.maxlength.rule : rules.maxlength;
return $q.resolve();
};
if (value.length > maxlength) {
var errorMessage = _.isObject(rules.maxlength) ? rules.maxlength.message : 'Must be fewer than ' + maxlength + ' characters';
/**
* Validates a value against the related rule-set (within validationRules).
* This method returns a promise to be resolved on successful validation.
* If validation fails the promise will be rejected with an error message.
* @memberof ModelValidator
* @param {Object} model Form-data object model is contained within
* @param {String} fieldName Name of field used to associate the rule-set map with a given value
* @param {Object} validationRules Set of named validation rules
* @returns {Promise} To be resolved or rejected based on validation success or failure.
*/
this.validateField = function(model, fieldName, validationRules) {
var rules = this.$getRulesForFieldName(validationRules, fieldName);
var value = NestedObjectHelper.readAttribute(model, fieldName);
return $q.reject(errorMessage);
if (rules) {
value = value || '';
if (rules.required) {
var required = angular.isObject(rules.required) ? rules.required.rule : rules.required;
if (!!value !== required) {
return $q.reject(
angular.isObject(rules.required) ?
rules.required.message :
FormForConfiguration.validationFailedForRequiredMessage);
}
}
}
if (rules.pattern) {
var pattern = _.isRegExp(rules.pattern) ? rules.pattern : rules.pattern.rule;
if (rules.minlength) {
var minlength = angular.isObject(rules.minlength) ? rules.minlength.rule : rules.minlength;
if (!pattern.exec(value)) {
var errorMessage = _.isRegExp(rules.pattern) ? 'Invalid format' : rules.pattern.message;
if (value.length < minlength) {
return $q.reject(
angular.isObject(rules.minlength) ?
rules.minlength.message :
$interpolate(FormForConfiguration.validationFailedForMinLengthMessage)({num: minlength}));
}
}
return $q.reject(errorMessage);
if (rules.maxlength) {
var maxlength = angular.isObject(rules.maxlength) ? rules.maxlength.rule : rules.maxlength;
if (value.length > maxlength) {
return $q.reject(
angular.isObject(rules.maxlength) ?
rules.maxlength.message :
$interpolate(FormForConfiguration.validationFailedForMaxLengthMessage)({num: maxlength}));
}
}
}
if (rules.custom) {
return rules.custom(value, model).then(
function(reason) {
return $q.resolve(reason);
},
function(reason) {
return $q.reject(reason || 'Failed custom validation');
});
if (rules.type) {
var type = angular.isObject(rules.type) ? rules.type.rule : rules.type;
var stringValue = value.toString();
if (type.indexOf('integer') >= 0 && !stringValue.match(/^\-*[0-9]+$/)) {
return $q.reject(
angular.isObject(rules.type) ?
rules.type.message :
FormForConfiguration.validationFailedForIntegerTypeMessage);
}
if (type.indexOf('number') >= 0 && !stringValue.match(/^\-*[0-9\.]+$/)) {
return $q.reject(
angular.isObject(rules.type) ?
rules.type.message :
FormForConfiguration.validationFailedForNumericTypeMessage);
}
if (type.indexOf('negative') >= 0 && !stringValue.match(/^\-[0-9\.]+$/)) {
return $q.reject(
angular.isObject(rules.type) ?
rules.type.message :
FormForConfiguration.validationFailedForNegativeTypeMessage);
}
if (type.indexOf('positive') >= 0 && !stringValue.match(/^[0-9\.]+$/)) {
return $q.reject(
angular.isObject(rules.type) ?
rules.type.message :
FormForConfiguration.validationFailedForPositiveTypeMessage);
}
if (type.indexOf('email') >= 0 && !stringValue.match(/^[\w\.\+]+@\w+\.\w+$/)) {
return $q.reject(
angular.isObject(rules.type) ?
rules.type.message :
FormForConfiguration.validationFailedForEmailTypeMessage);
}
}
if (rules.pattern) {
var isRegExp = rules.pattern instanceof RegExp;
var pattern = isRegExp ? rules.pattern : rules.pattern.rule;
if (!pattern.exec(value)) {
return $q.reject(
isRegExp ?
FormForConfiguration.validationFailedForPatternMessage :
rules.pattern.message);
}
}
if (rules.custom) {
var defaultErrorMessage = angular.isFunction(rules.custom) ? FormForConfiguration.validationFailedForCustomMessage : rules.custom.message;
var validationFunction = angular.isFunction(rules.custom) ? rules.custom : rules.custom.rule;
// Validations can fail in 3 ways:
// A promise that gets rejected (potentially with an error message)
// An error that gets thrown (potentially with a message)
// A falsy value
try {
var returnValue = validationFunction(value, model);
} catch (error) {
return $q.reject(error.message || defaultErrorMessage);
}
if (angular.isObject(returnValue) && angular.isFunction(returnValue.then)) {
return returnValue.then(
function(reason) {
return $q.resolve(reason);
},
function(reason) {
return $q.reject(reason || defaultErrorMessage);
});
} else if (returnValue) {
return $q.resolve(returnValue);
} else {
return $q.reject(defaultErrorMessage);
}
}
}
}
return $q.resolve();
};
return $q.resolve();
};
return this;
});
return this;
});
/**
* @ngdoc Services
* @name NestedObjectHelper
* @description
* Helper utility to simplify working with nested objects.
*/
angular.module('formFor').service('NestedObjectHelper', function($parse) {
return {
// For Angular 1.2.21 and below, $parse does not handle array brackets gracefully.
// Essentially we need to create Arrays that don't exist yet or objects within array indices that don't yet exist.
// @see https://github.com/angular/angular.js/issues/2845
$createEmptyArrays: function(object, attribute) {
var startOfArray = 0;
while (true) {
startOfArray = attribute.indexOf('[', startOfArray);
if (startOfArray < 0) {
break;
}
var arrayAttribute = attribute.substr(0, startOfArray);
var possibleArray = this.readAttribute(object, arrayAttribute);
// Create the Array if it doesn't yet exist
if (!possibleArray) {
possibleArray = [];
this.writeAttribute(object, arrayAttribute, possibleArray);
}
// Create an empty Object in the Array if the user is about to write to one (and one does not yet exist)
var match = attribute.substr(startOfArray).match(/([0-9]+)\]\./);
if (match) {
var index = parseInt(match[1]);
if (!possibleArray[index]) {
possibleArray[index] = {};
}
}
// Increment and keep scanning
startOfArray++;
}
},
flattenAttribute: function(attribute) {
return attribute.replace(/\./g, '___');
attribute = attribute.replace(/\[([^\]]+)\]\.{0,1}/g, '___$1___');
attribute = attribute.replace(/\./g, '___');
return attribute;
},

@@ -15,22 +61,36 @@

* Into an Array like ['foo', 'foo.bar', 'baz']
* @memberof NestedObjectHelper
* @param {Object} object Object to be flattened
* @returns {Array} Array of flattened keys (perhaps containing dot notation)
*/
flattenObjectKeys: function(object) {
var internalCrawler = function(object, path, array) {
array = array || [];
var keys = [];
var queue = [{
object: object,
prefix: null
}];
var prefix = path ? path + '.' : '';
while (true) {
if (queue.length === 0) {
break;
}
_.forIn(object,
function(value, relativeKey) {
var fullKey = prefix + relativeKey;
var data = queue.pop();
var prefix = data.prefix ? data.prefix + '.' : '';
array.push(fullKey);
if (typeof data.object === 'object') {
for (var prop in data.object) {
var path = prefix + prop;
internalCrawler(value, fullKey, array);
});
keys.push(path);
return array;
};
queue.push({
object: data.object[prop],
prefix: path
});
}
}
}
return internalCrawler(object);
return keys;
},

@@ -41,5 +101,6 @@

* This function guards against dot notation for nested references (ex. 'foo.bar').
*
* @param object Object
* @param attribute Attribute (or dot-notation path)
* @memberof NestedObjectHelper
* @param {Object} object Object ot be read
* @param {String} attribute Attribute (or dot-notation path) to read
* @returns {Object} Value defined at the specified key
*/

@@ -53,8 +114,10 @@ readAttribute: function(object, attribute) {

* This function guards against dot notation for nested references (ex. 'foo.bar').
*
* @param object Object
* @param attribute Attribute (or dot-notation path)
* @param value Value to be written
* @memberof NestedObjectHelper
* @param {Object} object Object ot be updated
* @param {String} attribute Attribute (or dot-notation path) to update
* @param {Object} value Value to be written
*/
writeAttribute: function(object, attribute, value) {
this.$createEmptyArrays(object, attribute);
$parse(attribute).assign(object, value);

@@ -61,0 +124,0 @@ }

/**
* @ngdoc Services
* @name $q
* @description
* Decorates the $q utility with additional methods used by formFor.
*
* @private

@@ -11,2 +13,5 @@ * This set of helper methods, small though they are, might be worth breaking apart into their own library?

* Similar to $q.reject, this is a convenience method to create and resolve a Promise.
* @memberof $q
* @param {Object} data Value to resolve the promise with
* @returns {Promise} A resolved promise
*/

@@ -22,2 +27,5 @@ $delegate.resolve = function(data) {

* Similar to $q.all but waits for all promises to resolve/reject before resolving/rejecting.
* @memberof $q
* @param {Array} promises Array of promises
* @returns {Promise} A promise to be resolved or rejected once all of the observed promises complete
*/

@@ -24,0 +32,0 @@ $delegate.waitForAll = function(promises) {

@@ -8,2 +8,3 @@ describe('ModelValidator', function() {

var $rootScope;
var FormForConfiguration;
var ModelValidator;

@@ -16,5 +17,6 @@ var model;

FormForConfiguration = $injector.get('FormForConfiguration');
ModelValidator = $injector.get('ModelValidator');
model = {
validationRules: {}
rules: {}
};

@@ -45,11 +47,11 @@ }));

it('should allow a value without a rule-set', function() {
model.validationRules = {};
model.rules = {};
expect(ModelValidator.validateField({foo: null}, 'foo', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({foo: undefined}, 'foo', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({foo: ''}, 'foo', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({foo: null}, 'foo', model.rules)).toBeResolved();
expect(ModelValidator.validateField({foo: undefined}, 'foo', model.rules)).toBeResolved();
expect(ModelValidator.validateField({foo: ''}, 'foo', model.rules)).toBeResolved();
});
it('should handle dot notation', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -62,9 +64,9 @@ bar: {

expect(ModelValidator.validateField({foo: { bar: null}}, 'foo.bar', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({foo: { bar: undefined}}, 'foo.bar', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({foo: { bar: ''}}, 'foo.bar', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({foo: { bar: null}}, 'foo.bar', model.rules)).toBeRejected();
expect(ModelValidator.validateField({foo: { bar: undefined}}, 'foo.bar', model.rules)).toBeRejected();
expect(ModelValidator.validateField({foo: { bar: ''}}, 'foo.bar', model.rules)).toBeRejected();
});
it('should reject a value that only passes some of the validations', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -76,3 +78,3 @@ required: true,

expect(ModelValidator.validateField({foo: '1'}, 'foo', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({foo: '1'}, 'foo', model.rules)).toBeRejected();
});

@@ -83,3 +85,3 @@ });

beforeEach(function() {
model.validationRules = {
model.rules = {
required: {

@@ -92,17 +94,17 @@ required: true

it('should reject null or undefined value', function() {
expect(ModelValidator.validateField({required: null}, 'required', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({required: undefined}, 'required', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({required: null}, 'required', model.rules)).toBeRejected();
expect(ModelValidator.validateField({required: undefined}, 'required', model.rules)).toBeRejected();
});
it('should reject an empty value', function() {
expect(ModelValidator.validateField({required: ''}, 'required', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({required: ''}, 'required', model.rules)).toBeRejected();
});
it('should allow a non-empty value', function() {
expect(ModelValidator.validateField({required: '1'}, 'required', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({required: true}, 'required', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({required: '1'}, 'required', model.rules)).toBeResolved();
expect(ModelValidator.validateField({required: true}, 'required', model.rules)).toBeResolved();
});
it('should allow custom error messages for failed validations', function() {
model.validationRules = {
model.rules = {
requiredField: {

@@ -117,3 +119,3 @@ required: {

verifyPromiseRejectedWithMessage(
ModelValidator.validateField({requiredField: null}, 'requiredField', model.validationRules),
ModelValidator.validateField({requiredField: null}, 'requiredField', model.rules),
'foobar');

@@ -125,3 +127,3 @@ });

beforeEach(function() {
model.validationRules = {
model.rules = {
minlengthField: {

@@ -134,21 +136,21 @@ minlength: 2

it('should reject null or undefined value', function() {
expect(ModelValidator.validateField({minlengthField: null}, 'minlengthField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({minlengthField: undefined}, 'minlengthField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({minlengthField: null}, 'minlengthField', model.rules)).toBeRejected();
expect(ModelValidator.validateField({minlengthField: undefined}, 'minlengthField', model.rules)).toBeRejected();
});
it('should reject an empty string', function() {
expect(ModelValidator.validateField({minlengthField: ''}, 'minlengthField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({minlengthField: ''}, 'minlengthField', model.rules)).toBeRejected();
});
it('should reject a string less than the required minimum length', function() {
expect(ModelValidator.validateField({minlengthField: '1'}, 'minlengthField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({minlengthField: '1'}, 'minlengthField', model.rules)).toBeRejected();
});
it('should allow a string greater than or equal to the required minimum length', function() {
expect(ModelValidator.validateField({minlengthField: '12'}, 'minlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({minlengthField: '123'}, 'minlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({minlengthField: '12'}, 'minlengthField', model.rules)).toBeResolved();
expect(ModelValidator.validateField({minlengthField: '123'}, 'minlengthField', model.rules)).toBeResolved();
});
it('should allow custom error messages for failed validations', function() {
model.validationRules = {
model.rules = {
minlengthField: {

@@ -163,3 +165,3 @@ minlength: {

verifyPromiseRejectedWithMessage(
ModelValidator.validateField({minlengthField: '1'}, 'minlengthField', model.validationRules),
ModelValidator.validateField({minlengthField: '1'}, 'minlengthField', model.rules),
'foobar');

@@ -171,3 +173,3 @@ });

beforeEach(function() {
model.validationRules = {
model.rules = {
maxlengthField: {

@@ -180,21 +182,21 @@ maxlength: 2

it('should allow a null or undefined value', function() {
expect(ModelValidator.validateField({maxlengthField: null}, 'maxlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: undefined}, 'maxlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: null}, 'maxlengthField', model.rules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: undefined}, 'maxlengthField', model.rules)).toBeResolved();
});
it('should allow an empty string', function() {
expect(ModelValidator.validateField({maxlengthField: ''}, 'maxlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: ''}, 'maxlengthField', model.rules)).toBeResolved();
});
it('should allow a string less or equal to than the maximum length', function() {
expect(ModelValidator.validateField({maxlengthField: '1'}, 'maxlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: '12'}, 'maxlengthField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: '1'}, 'maxlengthField', model.rules)).toBeResolved();
expect(ModelValidator.validateField({maxlengthField: '12'}, 'maxlengthField', model.rules)).toBeResolved();
});
it('should reject a string greater than the required maximum length', function() {
expect(ModelValidator.validateField({maxlengthField: '123'}, 'maxlengthField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({maxlengthField: '123'}, 'maxlengthField', model.rules)).toBeRejected();
});
it('should allow custom error messages for failed validations', function() {
model.validationRules = {
model.rules = {
maxlengthField: {

@@ -209,3 +211,3 @@ maxlength: {

verifyPromiseRejectedWithMessage(
ModelValidator.validateField({maxlengthField: '123'}, 'maxlengthField', model.validationRules),
ModelValidator.validateField({maxlengthField: '123'}, 'maxlengthField', model.rules),
'foobar');

@@ -215,5 +217,137 @@ });

describe('validateField type', function() {
beforeEach(function() {
model.rules = {
email: { type: 'email' },
integer: { type: 'integer' },
negative: { type: 'negative' },
number: { type: 'number' },
positive: { type: 'positive' }
};
});
it('number should accept numeric input', function() {
expect(ModelValidator.validateField({number: 123}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: -123}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: 1.23}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: -1.23}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: '123'}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: '-123'}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: '1.23'}, 'number', model.rules)).toBeResolved();
expect(ModelValidator.validateField({number: '-1.23'}, 'number', model.rules)).toBeResolved();
});
it('number should reject non-numeric input', function() {
expect(ModelValidator.validateField({number: 'abc'}, 'number', model.rules)).toBeRejected();
expect(ModelValidator.validateField({number: '1-1'}, 'number', model.rules)).toBeRejected();
expect(ModelValidator.validateField({number: '1a'}, 'number', model.rules)).toBeRejected();
});
it('integer should accept integer input', function() {
expect(ModelValidator.validateField({integer: 123}, 'integer', model.rules)).toBeResolved();
expect(ModelValidator.validateField({integer: -123}, 'integer', model.rules)).toBeResolved();
expect(ModelValidator.validateField({integer: '123'}, 'integer', model.rules)).toBeResolved();
expect(ModelValidator.validateField({integer: '-123'}, 'integer', model.rules)).toBeResolved();
});
it('integer should reject non-integer input', function() {
expect(ModelValidator.validateField({integer: 'abc'}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: '1a'}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: 1.23}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: -1.23}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: '1.23'}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: '-1.23'}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: '1-1'}, 'integer', model.rules)).toBeRejected();
expect(ModelValidator.validateField({integer: '.-11'}, 'integer', model.rules)).toBeRejected();
});
it('positive should accept positive numeric input', function() {
expect(ModelValidator.validateField({positive: 123}, 'positive', model.rules)).toBeResolved();
expect(ModelValidator.validateField({positive: 1.23}, 'positive', model.rules)).toBeResolved();
expect(ModelValidator.validateField({positive: '123'}, 'positive', model.rules)).toBeResolved();
expect(ModelValidator.validateField({positive: '1.23'}, 'positive', model.rules)).toBeResolved();
});
it('positive should reject negative numeric input', function() {
expect(ModelValidator.validateField({positive: -123}, 'positive', model.rules)).toBeRejected();
expect(ModelValidator.validateField({positive: -1.23}, 'positive', model.rules)).toBeRejected();
expect(ModelValidator.validateField({positive: '-123'}, 'positive', model.rules)).toBeRejected();
expect(ModelValidator.validateField({positive: '-1.23'}, 'positive', model.rules)).toBeRejected();
});
it('negative should accept negative numeric input', function() {
expect(ModelValidator.validateField({negative: -123}, 'negative', model.rules)).toBeResolved();
expect(ModelValidator.validateField({negative: -1.23}, 'negative', model.rules)).toBeResolved();
expect(ModelValidator.validateField({negative: '-123'}, 'negative', model.rules)).toBeResolved();
expect(ModelValidator.validateField({negative: '-1.23'}, 'negative', model.rules)).toBeResolved();
});
it('negative should reject negative numeric input', function() {
expect(ModelValidator.validateField({negative: 123}, 'negative', model.rules)).toBeRejected();
expect(ModelValidator.validateField({negative: 1.23}, 'negative', model.rules)).toBeRejected();
expect(ModelValidator.validateField({negative: '123'}, 'negative', model.rules)).toBeRejected();
expect(ModelValidator.validateField({negative: '1.23'}, 'negative', model.rules)).toBeRejected();
});
it('email should accept email input', function() {
expect(ModelValidator.validateField({email: 'abc@abc.com'}, 'email', model.rules)).toBeResolved();
expect(ModelValidator.validateField({email: 'abc.def@abc.com'}, 'email', model.rules)).toBeResolved();
expect(ModelValidator.validateField({email: 'abc+def@abc.com'}, 'email', model.rules)).toBeResolved();
});
it('email should reject non-email', function() {
expect(ModelValidator.validateField({email: 'abc'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: 'abc@'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: 'abc@abc'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: '@'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: '@abc'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: '@abc.'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: '@abc.com'}, 'email', model.rules)).toBeRejected();
expect(ModelValidator.validateField({email: 'abc.com'}, 'email', model.rules)).toBeRejected();
});
it('should allow custom error messages for failed email validations', function() {
model.rules = { email: { type: { rule: 'email', message: 'foobar email' } } };
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({email: null}, 'email', model.rules),
'foobar email');
});
it('should allow custom error messages for failed integer validations', function() {
model.rules = { integer: { type: { rule: 'integer', message: 'foobar integer' } } };
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({integer: null}, 'integer', model.rules),
'foobar integer');
});
it('should allow custom error messages for failed negative validations', function() {
model.rules = { negative: { type: { rule: 'negative', message: 'foobar negative' } } };
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({negative: null}, 'negative', model.rules),
'foobar negative');
});
it('should allow custom error messages for failed number validations', function() {
model.rules = { number: { type: { rule: 'number', message: 'foobar number' } } };
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({number: null}, 'number', model.rules),
'foobar number');
});
it('should allow custom error messages for failed positive validations', function() {
model.rules = { positive: { type: { rule: 'positive', message: 'foobar positive' } } };
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({positive: null}, 'positive', model.rules),
'foobar positive');
});
});
describe('validateField pattern', function() {
beforeEach(function() {
model.validationRules = {
model.rules = {
patternField: {

@@ -226,16 +360,16 @@ pattern: /[0-9]+/

it('should reject a null or undefined value', function() {
expect(ModelValidator.validateField({patternField: null}, 'patternField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({patternField: undefined}, 'patternField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({patternField: null}, 'patternField', model.rules)).toBeRejected();
expect(ModelValidator.validateField({patternField: undefined}, 'patternField', model.rules)).toBeRejected();
});
it('should reject strings not matching the specified pattern', function() {
expect(ModelValidator.validateField({patternField: 'abc'}, 'patternField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({patternField: 'abc'}, 'patternField', model.rules)).toBeRejected();
});
it('should allow strings matching the specified pattern', function() {
expect(ModelValidator.validateField({patternField: '123'}, 'patternField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({patternField: '123'}, 'patternField', model.rules)).toBeResolved();
});
it('should allow custom error messages for failed validations', function() {
model.validationRules = {
model.rules = {
patternField: {

@@ -250,3 +384,3 @@ pattern: {

verifyPromiseRejectedWithMessage(
ModelValidator.validateField({patternField: 'abc'}, 'patternField', model.validationRules),
ModelValidator.validateField({patternField: 'abc'}, 'patternField', model.rules),
'foobar');

@@ -258,3 +392,3 @@ });

beforeEach(function() {
model.validationRules = {
model.rules = {
customField: {

@@ -269,10 +403,10 @@ custom: function(value) {

it('should reject values rejected by the custom validator', function() {
expect(ModelValidator.validateField({customField: 'allowed'}, 'customField', model.validationRules)).toBeResolved();
expect(ModelValidator.validateField({customField: 'allowed'}, 'customField', model.rules)).toBeResolved();
});
it('should allow values accepted by the custom validator', function() {
expect(ModelValidator.validateField({customField: null}, 'customField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({customField: undefined}, 'customField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({customField: ''}, 'customField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({customField: 'not-alllowed'}, 'customField', model.validationRules)).toBeRejected();
expect(ModelValidator.validateField({customField: null}, 'customField', model.rules)).toBeRejected();
expect(ModelValidator.validateField({customField: undefined}, 'customField', model.rules)).toBeRejected();
expect(ModelValidator.validateField({customField: ''}, 'customField', model.rules)).toBeRejected();
expect(ModelValidator.validateField({customField: 'not-alllowed'}, 'customField', model.rules)).toBeRejected();
});

@@ -283,3 +417,3 @@

model.validationRules = {
model.rules = {
customField: {

@@ -298,3 +432,3 @@ custom: function(value, formData) {

ModelValidator.validateField(formData, 'customField', model.validationRules);
ModelValidator.validateField(formData, 'customField', model.rules);

@@ -304,4 +438,144 @@ expect(valueParameter).toEqual(value);

});
it('should reject a custom validation that is not a function', function() {
model.rules = {
customField: {
custom: true
}
};
expect(ModelValidator.validateField({customField: 'allowed'}, 'customField', model.rules)).toBeRejected();
});
it('should treat truthy values as successful validations', function() {
model.rules = {
customField: {
custom: function() {
return true;
}
}
};
expect(ModelValidator.validateField({customField: 'allowed'}, 'customField', model.rules)).toBeResolved();
});
it('should treat falsy values as failed validations', function() {
model.rules = {
customField: {
custom: function() {
return false;
}
}
};
expect(ModelValidator.validateField({customField: 'allowed'}, 'customField', model.rules)).toBeRejected();
});
it('should support inline custom error messages for failed falsy validations', function() {
model.rules = {
customField: {
custom: {
rule: function() {
return false;
},
message: 'failed custom'
}
}
};
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({customField: 'abc'}, 'customField', model.rules),
'failed custom');
});
it('should support custom validations that throw errors with custom error messages for failed validations', function() {
model.rules = {
customField: {
custom: function() {
throw Error('i am an error');
}
}
};
verifyPromiseRejectedWithMessage(
ModelValidator.validateField({customField: 'abc'}, 'customField', model.rules),
'i am an error');
});
});
describe('$getRulesForFieldName', function() {
beforeEach(function() {
model.rules = {
things: {
collection: {
fields: {
name: {
required: true
}
}
}
},
thing: {
name: {
required: true
}
}
};
});
it('should strip array brackets from collection field names', function() {
expect(ModelValidator.$getRulesForFieldName(model.rules, 'things[0].name')).toEqual(model.rules.things.collection.fields.name);
});
it('should not modify field anmes without array brackets', function() {
expect(ModelValidator.$getRulesForFieldName(model.rules, 'thing.name')).toEqual(model.rules.thing.name);
});
});
describe('validateCollection and validateField for collections', function() {
beforeEach(function() {
model.rules = {
things: {
collection: {
min: 2,
max: 4
}
}
};
});
it('should strip array brackets from collection field names', function() {
model.rules.things.collection = { fields: { name: { required: true } } };
expect(ModelValidator.validateField({things: null}, 'things[0].name', model.rules)).toBeRejected();
expect(ModelValidator.validateField({things: [{name: 'Brian'}]}, 'things[0].name', model.rules)).toBeResolved();
});
it('should validate collections size min/max', function() {
expect(ModelValidator.validateCollection({}, 'things', model.rules)).toBeRejected();
expect(ModelValidator.validateCollection({things: null}, 'things', model.rules)).toBeRejected();
expect(ModelValidator.validateCollection({things: []}, 'things', model.rules)).toBeRejected();
expect(ModelValidator.validateCollection({things: [{}]}, 'things', model.rules)).toBeRejected();
expect(ModelValidator.validateCollection({things: [{},{}]}, 'things', model.rules)).toBeResolved();
expect(ModelValidator.validateCollection({things: [{},{},{}]}, 'things', model.rules)).toBeResolved();
expect(ModelValidator.validateCollection({things: [{},{},{},{}]}, 'things', model.rules)).toBeResolved();
expect(ModelValidator.validateCollection({things: [{},{},{},{},{}]}, 'things', model.rules)).toBeRejected();
});
it('should validate custom collections validation error messages', function() {
model.rules.things.collection.min = {rule: 2, message: 'custom min'};
model.rules.things.collection.max = {rule: 4, message: 'custom max'};
verifyPromiseRejectedWithMessage(
ModelValidator.validateCollection({things: []}, 'things', model.rules),
'custom min');
verifyPromiseRejectedWithMessage(
ModelValidator.validateCollection({things: [{},{},{},{},{}]}, 'things', model.rules),
'custom max');
});
}); // validateCollection
describe('validateFields', function() {

@@ -311,3 +585,3 @@ it('should validate only fields specified by the whitelist', function() {

Object.defineProperty(model.validationRules, 'foo', {
Object.defineProperty(model.rules, 'foo', {
get: function() {

@@ -319,3 +593,3 @@ fooCalled = true;

});
Object.defineProperty(model.validationRules, 'bar', {
Object.defineProperty(model.rules, 'bar', {
get: function() {

@@ -327,3 +601,3 @@ barCalled = true;

});
Object.defineProperty(model.validationRules, 'baz', {
Object.defineProperty(model.rules, 'baz', {
get: function() {

@@ -336,3 +610,3 @@ bazCalled = true;

ModelValidator.validateFields({}, ['foo', 'bar'], model.validationRules);
ModelValidator.validateFields({}, ['foo', 'bar'], model.rules);

@@ -347,3 +621,3 @@ expect(fooCalled).toBeTruthy();

model.validationRules = {
model.rules = {
foo: {

@@ -378,3 +652,3 @@ minlength: 1

ModelValidator.validateFields(formData, ['foo'], model.validationRules);
ModelValidator.validateFields(formData, ['foo'], model.rules);

@@ -387,3 +661,3 @@ expect(fooCalled).toBeTruthy();

it('should handle dot notation', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -404,3 +678,3 @@ bar: {

}
}, ['foo.bar', 'foo.baz'], model.validationRules);
}, ['foo.bar', 'foo.baz'], model.rules);

@@ -422,13 +696,59 @@ expect(promise).toBeRejected();

});
});
it('should resolve if model matches all of the specified rules', function() {
model.rules = {
foo: {
collection: { min: 2 }
},
bar: {
collection: { max: 4 }
}
};
expect(ModelValidator.validateFields({
foo: ['1','2'],
bar: ['1','2']
}, ['foo', 'bar'], model.rules)).toBeResolved();
});
it('should reject if model does not match any of the specified rules', function() {
model.rules = {
foo: {
collection: { min: 2 }
},
bar: {
collection: { max: 4 }
}
};
var promise = ModelValidator.validateFields({
foo: ['1'],
bar: ['1','2','3','4','5']
}, ['foo', 'bar'], model.rules);
expect(promise).toBeRejected();
var errorMap;
promise.then(
angular.noop,
function(value) {
errorMap = value;
});
$rootScope.$apply(); // Trigger Promise resolution
expect(errorMap.foo).toBeTruthy();
expect(errorMap.bar).toBeTruthy();
});
}); // validateFields
describe('validateAll', function() {
it('should resolve on an empty set of fields', function() {
model.validationRules = {};
model.rules = {};
expect(ModelValidator.validateAll({}, model.validationRules)).toBeResolved();
expect(ModelValidator.validateAll({}, model.rules)).toBeResolved();
});
it('should allow all values if model does not specify rule sets', function() {
model.validationRules = {};
model.rules = {};

@@ -438,7 +758,7 @@ expect(ModelValidator.validateAll({

bar: true
}, model.validationRules)).toBeResolved();
}, model.rules)).toBeResolved();
});
it('should resolve if model matches all of the specified rules', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -463,7 +783,7 @@ required: true,

baz: 'allowed'
}, model.validationRules)).toBeResolved();
}, model.rules)).toBeResolved();
});
it('should reject if model does not match any of the specified rules', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -488,3 +808,3 @@ required: true,

baz: true
}, model.validationRules);
}, model.rules);

@@ -508,3 +828,3 @@ expect(promise).toBeRejected();

it('should handle dot notation', function() {
model.validationRules = {
model.rules = {
foo: {

@@ -525,3 +845,3 @@ bar: {

}
}, model.validationRules);
}, model.rules);

@@ -544,2 +864,81 @@ expect(promise).toBeRejected();

});
describe('FormForConfiguration custom validation error messages', function() {
var testCustomValidationFailureMessage = function(validationAttribute, validationValue, objectValue, expectedMessage) {
var rules = {};
rules.field = {};
rules.field[validationAttribute] = validationValue;
var object = {};
object.field = objectValue;
verifyPromiseRejectedWithMessage(
ModelValidator.validateField(object, 'field', rules),
expectedMessage);
};
it('should allow overrides for required', function() {
FormForConfiguration.setValidationFailedForRequiredMessage('custom required');
testCustomValidationFailureMessage('required', true, null, 'custom required');
});
it('should allow overrides for minlength', function() {
FormForConfiguration.setValidationFailedForMinLengthMessage('custom minlength');
testCustomValidationFailureMessage('minlength', 2, '1', 'custom minlength');
});
it('should allow overrides for maxlength', function() {
FormForConfiguration.setValidationFailedForMaxLengthMessage('custom maxlength');
testCustomValidationFailureMessage('maxlength', 2, '123', 'custom maxlength');
});
it('should allow overrides for pattern', function() {
FormForConfiguration.setValidationFailedForPatternMessage('custom pattern');
testCustomValidationFailureMessage('pattern', /a/, '123', 'custom pattern');
});
it('should allow overrides for type integer', function() {
FormForConfiguration.setValidationFailedForIntegerTypeMessage('custom type integer');
testCustomValidationFailureMessage('type', 'integer', 'invalid', 'custom type integer');
});
it('should allow overrides for type number', function() {
FormForConfiguration.setValidationFailedForNumericTypeMessage('custom type number');
testCustomValidationFailureMessage('type', 'number', 'invalid', 'custom type number');
});
it('should allow overrides for type negative', function() {
FormForConfiguration.setValidationFailedForNegativeTypeMessage('custom type negative');
testCustomValidationFailureMessage('type', 'negative integer', '1', 'custom type negative');
});
it('should allow overrides for type positive', function() {
FormForConfiguration.setValidationFailedForPositiveTypeMessage('custom type positive');
testCustomValidationFailureMessage('type', 'positive integer', '-1', 'custom type positive');
});
it('should allow overrides for type email', function() {
FormForConfiguration.setValidationFailedForEmailTypeMessage('custom type email');
testCustomValidationFailureMessage('type', 'email', 'invalid', 'custom type email');
});
it('should allow overrides for custom', function() {
FormForConfiguration.setValidationFailedForCustomMessage('custom custom');
var custom = function() {
return $q.reject();
};
testCustomValidationFailureMessage('custom', custom, null, 'custom custom');
});
});
});

@@ -17,5 +17,9 @@ describe('NestedObjectHelper', function() {

it('should not correct a string without dot notation', function() {
it('should not adjust a string without dot notation', function() {
expect(NestedObjectHelper.flattenAttribute('foo')).toMatch('foo');
});
it('should handle array notation', function() {
expect(NestedObjectHelper.flattenAttribute('foo[1].bar')).toMatch('foo___1___bar');
});
});

@@ -72,2 +76,19 @@

});
it('should handle array notation when array is empty or non-existent', function() {
object = {
empty: []
};
expect(NestedObjectHelper.readAttribute(object, 'empty[0]')).toBeFalsy();
expect(NestedObjectHelper.readAttribute(object, 'nonexistent[0]')).toBeFalsy();
});
it('should handle array notation by reading values from an array at the specified index', function() {
object = {
array: ['one']
};
expect(NestedObjectHelper.readAttribute(object, 'array[0]')).toMatch('one');
});
});

@@ -100,4 +121,53 @@

});
it('should handle array notation by creating arrays that do not yet exist', function() {
NestedObjectHelper.writeAttribute(object, 'collection[0]', 'first item');
expect(object.collection).toBeTruthy();
expect(object.collection[0]).toMatch('first item');
});
it('should handle array notation by creating indexes that do not yet exist', function() {
object.collection = ['one'];
NestedObjectHelper.writeAttribute(object, 'collection[1]', 'two');
expect(object.collection).toBeTruthy();
expect(object.collection[0]).toMatch('one');
expect(object.collection[1]).toMatch('two');
});
it('should handle array notation with nested objects for indexes that do not yet exist', function() {
object.collection = [];
NestedObjectHelper.writeAttribute(object, 'collection[0].number', 'one');
expect(object.collection).toBeTruthy();
expect(object.collection[0].number).toMatch('one');
});
it('should handle array notation by writing values to an array at the specified index', function() {
object.collection = ['old'];
NestedObjectHelper.writeAttribute(object, 'collection[0]', 'new');
expect(object.collection).toBeTruthy();
expect(object.collection[0]).toMatch('new');
});
it('should handle array notation with nested objects for indexes that already exist', function() {
object.collection = [{
foo: 'FOO',
bar: 'BAR'
}];
NestedObjectHelper.writeAttribute(object, 'collection[0].bar', 'RAB');
NestedObjectHelper.writeAttribute(object, 'collection[0].baz', 'BAZ');
expect(object.collection).toBeTruthy();
expect(object.collection[0].foo).toMatch('FOO');
expect(object.collection[0].bar).toMatch('RAB');
expect(object.collection[0].baz).toMatch('BAZ');
});
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc