;
96 |
97 | /**
98 | * The form data is valid based on the specified validation rules.
99 | */
100 | valid:boolean;
101 |
102 | /**
103 | * Controls form validation behavior.
104 | * Acceptable values include: change, submit, manual.
105 | * Forms validate on-change by default, which is to say that validation is run anytime a field-value is changed.
106 | * To defver validation until the form is submitted, use "submit" and to disable auto-validation entirely use "manual".
107 | */
108 | validateOn?:string;
109 |
110 | /**
111 | * Optional callback to be invoked whenever a form-submit is blocked due to a failed validation.
112 | */
113 | validationFailed?:Function;
114 |
115 | /**
116 | * Set of client-side validation rules (keyed by form field names) to apply to form-data before submitting.
117 | * For more information refer to the Validation Types page.
118 | *
119 | * @private
120 | * This is the data (optionally) specified by the user as an HTML attribute.
121 | * The value actually consumed by the formFor directive is '$validationRuleset'.
122 | */
123 | validationRules?:ValidationRuleSet;
124 | }
125 | }
--------------------------------------------------------------------------------
/source/interfaces/submit-button-wrapper.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Object containing keys to be observed by the input button.
5 | */
6 | export interface SubmitButtonWrapper {
7 |
8 | /**
9 | * Button should disable itself if this value becomes true; typically this means the form is being submitted.
10 | */
11 | disabled:boolean;
12 | };
13 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-error-map.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Map of field names to error messages describing validation failures.
5 | *
6 | * Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationErrorMap {
9 | [fieldName:string]:string;
10 | };
11 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-boolean.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Associates a boolean validation rule with a custom failure message.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleBoolean {
9 |
10 | /**
11 | * This rule is active.
12 | * If the condition it applies to is not met, the field should be considered invalid.
13 | */
14 | rule:boolean;
15 |
16 | /**
17 | * Custom error message to be shown for failed validations.
18 | */
19 | message:string;
20 | };
21 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-collection.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Describes rules for validating a form-field that contains a collection.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleCollection {
9 |
10 | /**
11 | * Rules for validating properties of objects within the current collection.
12 | * See {@link ValidationRules}.
13 | */
14 | fields?:ValidationRuleSet;
15 |
16 | /**
17 | * The collection must contain no more than this many items.
18 | */
19 | max?:number|ValidationRuleNumber;
20 |
21 | /**
22 | * The collection must contain at least this many items.
23 | */
24 | min?:number|ValidationRuleNumber;
25 | };
26 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-custom.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Associates a custom validation rule with a custom failure message.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleCustom {
9 |
10 | /**
11 | * Custom validation function.
12 | * If this function returns a rejected promise or a falsy value, the field should be considered invalid.
13 | */
14 | rule:CustomValidationFunction;
15 |
16 | /**
17 | * Custom error message to be shown for failed validations.
18 | */
19 | message:string;
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-field-type.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Associates a field-type validation rule with a custom failure message.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleFieldType {
9 |
10 | /**
11 | * The required field type.
12 | * If the condition it applies to is not met, the field should be considered invalid.
13 | */
14 | rule:ValidationFieldType;
15 |
16 | /**
17 | * Custom error message to be shown for failed validations.
18 | */
19 | message:string;
20 | };
21 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-number.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Associates a numeric validation rule with a custom failure message.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleNumber {
9 |
10 | /**
11 | * Describes the numeric constraint to be applied to the associated field.
12 | * If the condition is not met, the field should be considered invalid.
13 | */
14 | rule:number;
15 |
16 | /**
17 | * Custom error message to be shown for failed validations.
18 | */
19 | message:string;
20 | };
21 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule-regexp.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Associates a RegExp validation rule with a custom failure message.
5 | *
6 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
7 | */
8 | export interface ValidationRuleRegExp {
9 |
10 | /**
11 | * Describes the regular expression condition to be applied to the associated field.
12 | * If the condition is not met, the field should be considered invalid.
13 | */
14 | rule:RegExp;
15 |
16 | /**
17 | * Custom error message to be shown for failed validations.
18 | */
19 | message:string;
20 | };
21 | };
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-rule.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | module formFor {
4 |
5 | /**
6 | * Describes rules for validating a single form-field.
7 | *
8 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
9 | */
10 | export interface ValidationRules {
11 |
12 | /**
13 | * Special validation rules for fields that are collections.
14 | * For more information see {@link ValidationRuleCollection}.
15 | */
16 | collection?:ValidationRuleCollection;
17 |
18 | /**
19 | * Custom validation function.
20 | * Field must be approved by this function in order to be considered valid.
21 | * Approval can be indicated by a return value of TRUE or a Promise that resolves.
22 | * Rejected validation Promises may supply a custom error message.
23 | */
24 | custom?:CustomValidationFunction|ValidationRuleCustom;
25 |
26 | /**
27 | * Numeric values can be further defined with an increment.
28 | * Values not matching this increment will not be accepted.
29 | * For example, for an increment of 2, values 0, 2, and 4 would be considered valid but 1 or 3 would not.
30 | * This field can be either a number or an instance of {@link ValidationRuleNumber}.
31 | */
32 | increment?:number|ValidationRuleNumber;
33 |
34 | /**
35 | * Field's numeric value must be lesser than or equal to this value.
36 | * This field can be either a number or an instance of {@link ValidationRuleNumber}.
37 | */
38 | maximum?:Function|number|ValidationRuleNumber;
39 |
40 | /**
41 | * Field must contain no more than this many characters.
42 | * This field can be either a number or an instance of {@link ValidationRuleNumber}.
43 | */
44 | maxlength?:number|ValidationRuleNumber;
45 |
46 | /**
47 | * Field's numeric value must be greater than or equal to this value.
48 | * This field can be either a number or an instance of {@link ValidationRuleNumber}.
49 | */
50 | minimum?:Function|number|ValidationRuleNumber;
51 |
52 | /**
53 | * Field must contain at least this many characters.
54 | * This field can be either a number or an instance of {@link ValidationRuleNumber}.
55 | */
56 | minlength?:number|ValidationRuleNumber;
57 |
58 | /**
59 | * Field contains a string that much match this regular expression pattern.
60 | * This field can be either a RegExp or an instance of {@link ValidationRuleRegExp}.
61 | */
62 | pattern?:RegExp|ValidationRuleRegExp;
63 |
64 | /**
65 | * Is this field required?
66 | * This field can be either a boolean or an instance of {@link ValidationRuleBoolean}.
67 | */
68 | required?:Function|boolean|ValidationRuleBoolean;
69 |
70 | /**
71 | * Enumeration for common validation types.
72 | * Supported types are described in {@link ValidationFieldType}.
73 | * You can also specify multiple types (ex. "positive integer").
74 | */
75 | type?:ValidationFieldType|ValidationRuleFieldType;
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/source/interfaces/validation/validation-ruleset.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Maps one or more attributes to a set of validation rules governing their behavior.
5 | * See {@link ValidationRules}.
6 | *
7 | *
Note that this interface exists for type-checking only; nothing actually implements this interface.
8 | */
9 | export interface ValidationRuleSet {
10 | [fieldName:string]:ValidationRules;
11 | };
12 | };
--------------------------------------------------------------------------------
/source/module.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | angular.module('formFor', []);
--------------------------------------------------------------------------------
/source/services/field-helper.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | module formFor {
4 |
5 | /**
6 | * Various helper methods for functionality shared between formFor field directives.
7 | */
8 | export class FieldHelper {
9 |
10 | private formForConfiguration_:FormForConfiguration;
11 |
12 | constructor(FormForConfiguration) {
13 | this.formForConfiguration_ = FormForConfiguration;
14 | }
15 |
16 | /**
17 | * Determines the field's label based on its current attributes and the FormForConfiguration configuration settings.
18 | * Also watches for changes in the (attributes) label and updates $scope accordingly.
19 | *
20 | * @param $scope Directive link $scope
21 | * @param $attributes Directive link $attributes
22 | * @param humanizeValueAttribute Fall back to a humanized version of the :value attribute if no label is provided;
23 | * This can be useful for radio options where the label should represent the value.
24 | * By default, a humanized version of the :attribute attribute will be used.
25 | */
26 | manageLabel($scope:ng.IScope, $attributes:ng.IAttributes, humanizeValueAttribute:boolean):void {
27 | if (this.formForConfiguration_.autoGenerateLabels) {
28 | $scope['label'] =
29 | humanizeValueAttribute ?
30 | StringUtil.humanize($scope['value']) :
31 | StringUtil.humanize($scope['attribute']);
32 | }
33 |
34 | if (this.formForConfiguration_.labelClass) {
35 | $scope['labelClass'] =
36 | this.formForConfiguration_.labelClass;
37 | }
38 |
39 | if ($attributes.hasOwnProperty('label')) {
40 | $attributes.$observe('label', function (label) {
41 | $scope['label'] = label;
42 | });
43 | }
44 |
45 | if ($attributes.hasOwnProperty('labelClass')) {
46 | $attributes.$observe('labelClass', function (labelClass) {
47 | $scope['labelClass'] = labelClass;
48 | });
49 | }
50 | }
51 |
52 | /**
53 | * Helper method that registers a form field and stores the bindable object returned on the $scope.
54 | * This method also unregisters the field on $scope $destroy.
55 | *
56 | * @param $scope Input field $scope
57 | * @param $attributes Input field $attributes element
58 | * @param formForController Controller object for parent formFor
59 | */
60 | manageFieldRegistration($scope:ng.IScope, $attributes:ng.IAttributes, formForController:FormForController):void {
61 | $scope.$watch('attribute', function (newValue, oldValue) {
62 | if ($scope['model']) {
63 | formForController.unregisterFormField(oldValue);
64 | }
65 |
66 | $scope['model'] = formForController.registerFormField($scope['attribute']);
67 |
68 | if ($attributes['uid']) { // Optional override ~ issue #57
69 | $scope['model']['uid'] = $attributes['uid'];
70 | }
71 | });
72 |
73 | $scope.$on('$destroy', function () {
74 | formForController.unregisterFormField($scope['attribute']);
75 | });
76 | }
77 | }
78 |
79 | angular.module('formFor').service('FieldHelper',
80 | (FormForConfiguration) => new FieldHelper(FormForConfiguration));
81 | }
--------------------------------------------------------------------------------
/source/utils/form-for-guid.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * UID generator for formFor input fields.
5 | * @see http://stackoverflow.com/questions/6248666/how-to-generate-short-uid-like-ax4j9z-in-js
6 | *
7 | *
Intended for use only by formFor directive; this class is not exposed to the $injector.
8 | */
9 | export class FormForGUID {
10 |
11 | /**
12 | * Create a new GUID.
13 | */
14 | public static create():string {
15 | return ("0000" + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4);
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/source/utils/form-for-state-helper.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | module formFor {
4 |
5 | /*
6 | * Organizes state management for form-submission and field validity.
7 | *
8 | *
Intended for use only by formFor directive; this class is not exposed to the $injector.
9 | */
10 | export class FormForStateHelper {
11 |
12 | private fieldNameToErrorMap_:{[fieldName:string]:string};
13 | private fieldNameToModifiedStateMap_:{[fieldName:string]:boolean};
14 | private formForScope_:FormForScope;
15 | private formSubmitted_:boolean;
16 | private nestedObjectHelper_:NestedObjectHelper;
17 |
18 | /**
19 | * $scope.$watch this value for notification of form-state changes.
20 | */
21 | public watchable:number;
22 |
23 | // TODO Add some documentation
24 | constructor($parse:ng.IParseService, $scope:FormForScope) {
25 | this.formForScope_ = $scope;
26 | this.nestedObjectHelper_ = new NestedObjectHelper($parse);
27 |
28 | this.formForScope_.fieldNameToErrorMap = $scope.fieldNameToErrorMap || {};
29 | this.formForScope_.valid = true;
30 |
31 | this.fieldNameToModifiedStateMap_ = {};
32 | this.formSubmitted_ = false;
33 | this.fieldNameToErrorMap_ = {};
34 | this.watchable = 0;
35 | }
36 |
37 | public getFieldError(fieldName:string):string {
38 | return this.nestedObjectHelper_.readAttribute(this.formForScope_.fieldNameToErrorMap, fieldName);
39 | }
40 |
41 | public hasFieldBeenModified(fieldName:string):boolean {
42 | return this.nestedObjectHelper_.readAttribute(this.fieldNameToModifiedStateMap_, fieldName);
43 | }
44 |
45 | public hasFormBeenSubmitted():boolean {
46 | return this.formSubmitted_;
47 | }
48 |
49 | public isFormInvalid():boolean {
50 | return !this.isFormValid();
51 | }
52 |
53 | public isFormValid():boolean {
54 | for (var prop in this.fieldNameToErrorMap_) {
55 | return false;
56 | }
57 |
58 | return true;
59 | }
60 |
61 | public resetFieldErrors():void {
62 | this.formForScope_.fieldNameToErrorMap = {};
63 | }
64 |
65 | public setFieldError(fieldName:string, error:string):void {
66 | var safeFieldName:string = this.nestedObjectHelper_.flattenAttribute(fieldName);
67 |
68 | this.nestedObjectHelper_.writeAttribute(this.formForScope_.fieldNameToErrorMap, fieldName, error);
69 |
70 | if (error) {
71 | this.fieldNameToErrorMap_[safeFieldName] = error;
72 | } else {
73 | delete this.fieldNameToErrorMap_[safeFieldName];
74 | }
75 |
76 | this.formForScope_.valid = this.isFormValid();
77 | this.watchable++;
78 | }
79 |
80 | public setFieldHasBeenModified(fieldName:string, hasBeenModified:boolean):void {
81 | this.nestedObjectHelper_.writeAttribute(this.fieldNameToModifiedStateMap_, fieldName, hasBeenModified);
82 |
83 | this.watchable++;
84 | }
85 |
86 | public setFormSubmitted(submitted:boolean):void {
87 | this.formSubmitted_ = submitted;
88 | this.watchable++;
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/source/utils/nested-object-helper.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | module formFor {
4 |
5 | /**
6 | * Helper utility to simplify working with nested objects.
7 | *
8 | *
Intended for use only by formFor directive; this class is not exposed to the $injector.
9 | */
10 | export class NestedObjectHelper {
11 |
12 | private $parse_:ng.IParseService;
13 |
14 | /**
15 | * Constructor.
16 | *
17 | * @param $parse Injector-supplied $parse service
18 | */
19 | constructor($parse:ng.IParseService) {
20 | this.$parse_ = $parse;
21 | }
22 |
23 | /**
24 | * Converts a field name (which may contain dots or array indices) into a string that can be used to key an object.
25 | * e.g. a field name like 'items[0].name' would be converted into 'items___0______name'
26 | *
27 | * @param fieldName Attribute (or dot-notation path) to read
28 | * @returns Modified field name safe to use as an object key
29 | */
30 | public flattenAttribute(fieldName:string):string {
31 | return fieldName.replace(/\[([^\]]+)\]\.{0,1}/g, '___$1___').replace(/\./g, '___');
32 | }
33 |
34 | /**
35 | * Crawls an object and returns a flattened set of all attributes using dot notation.
36 | * This converts an Object like: {foo: {bar: true}, baz: true} into an Array like ['foo', 'foo.bar', 'baz'].
37 | *
38 | * @param object Object to be flattened
39 | * @returns Array of flattened keys (perhaps containing dot notation)
40 | */
41 | public flattenObjectKeys(object:any):Array {
42 | var keys:Array = [];
43 | var queue:Array = [{
44 | object: object,
45 | prefix: null
46 | }];
47 |
48 | while (true) {
49 | if (queue.length === 0) {
50 | break;
51 | }
52 |
53 | var data:any = queue.pop();
54 | var objectIsArray = Array.isArray(data.object);
55 | var prefix:string = data.prefix ? data.prefix + ( objectIsArray ? '[' : '.' ) : '';
56 | var suffix:string = objectIsArray ? ']' : '';
57 |
58 | if (typeof data.object === 'object') {
59 | for (var prop in data.object) {
60 | var path:string = prefix + prop + suffix;
61 | var value:any = data.object[prop];
62 |
63 | keys.push(path);
64 |
65 | queue.push({
66 | object: value,
67 | prefix: path
68 | });
69 | }
70 | }
71 | }
72 |
73 | return keys;
74 | }
75 |
76 | /**
77 | * Returns the value defined by the specified attribute.
78 | * This function guards against dot notation for nested references (ex. 'foo.bar').
79 | *
80 | * @param object Object ot be read
81 | * @param fieldName Attribute (or dot-notation path) to read
82 | * @returns Value defined at the specified key
83 | */
84 | public readAttribute(object:any, fieldName:string):any {
85 | return this.$parse_(fieldName)(object);
86 | }
87 |
88 | /**
89 | * Writes the specified value to the specified attribute.
90 | * This function guards against dot notation for nested references (ex. 'foo.bar').
91 | *
92 | * @param object Object ot be updated
93 | * @param fieldName Attribute (or dot-notation path) to update
94 | * @param value Value to be written
95 | */
96 | public writeAttribute(object:any, fieldName:string, value:any):void {
97 | this.initializeArraysAndObjectsForParse_(object, fieldName);
98 | this.$parse_(fieldName).assign(object, value);
99 | }
100 |
101 | // Helper methods ////////////////////////////////////////////////////////////////////////////////////////////////////
102 |
103 | // For Angular 1.2.21 and below, $parse does not handle array brackets gracefully.
104 | // Essentially we need to create Arrays that don't exist yet or objects within array indices that don't yet exist.
105 | // @see https://github.com/angular/angular.js/issues/2845
106 | private initializeArraysAndObjectsForParse_(object:any, attribute:string):void {
107 | var startOfArray:number = 0;
108 |
109 | while (true) {
110 | startOfArray = attribute.indexOf('[', startOfArray);
111 |
112 | if (startOfArray < 0) {
113 | break;
114 | }
115 |
116 | var arrayAttribute:string = attribute.substr(0, startOfArray);
117 | var possibleArray:any = this.readAttribute(object, arrayAttribute);
118 |
119 | // Create the Array if it doesn't yet exist
120 | if (!possibleArray) {
121 | possibleArray = [];
122 |
123 | this.writeAttribute(object, arrayAttribute, possibleArray);
124 | }
125 |
126 | // Create an empty Object in the Array if the user is about to write to one (and one does not yet exist)
127 | var match:Array = attribute.substr(startOfArray).match(/([0-9]+)\]\./);
128 |
129 | if (match) {
130 | var targetIndex:number = parseInt(match[1]);
131 |
132 | if (!possibleArray[targetIndex]) {
133 | possibleArray[targetIndex] = {};
134 | }
135 | }
136 |
137 | // Increment and keep scanning
138 | startOfArray++;
139 | }
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------
/source/utils/promise-utils.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Supplies $q service with additional methods.
5 | *
6 | * Intended for use only by formFor directive; this class is not exposed to the $injector.
7 | */
8 | export class PromiseUtils implements ng.IQService {
9 |
10 | private $q_:ng.IQService;
11 |
12 | /**
13 | * Constructor.
14 | *
15 | * @param $q Injector-supplied $q service
16 | */
17 | constructor($q:ng.IQService) {
18 | this.$q_ = $q;
19 | }
20 |
21 | /**
22 | * @inheritDoc
23 | */
24 | all(promises:ng.IPromise[]):ng.IPromise {
25 | return this.$q_.all(promises);
26 | }
27 |
28 | /**
29 | * @inheritDoc
30 | */
31 | defer():ng.IDeferred {
32 | return this.$q_.defer();
33 | }
34 |
35 | /**
36 | * Similar to $q.reject, this is a convenience method to create and resolve a Promise.
37 | *
38 | * @param data Value to resolve the promise with
39 | * @returns A resolved promise
40 | */
41 | resolve(data?:any):ng.IPromise {
42 | var deferred:ng.IDeferred = this.$q_.defer();
43 | deferred.resolve(data);
44 |
45 | return deferred.promise;
46 | }
47 |
48 | /**
49 | * @inheritDoc
50 | */
51 | reject(reason?:any):ng.IPromise {
52 | return this.$q_.reject(reason);
53 | }
54 |
55 | /**
56 | * Similar to $q.all but waits for all promises to resolve/reject before resolving/rejecting.
57 | *
58 | * @param promises Array of Promises
59 | * @returns A promise to be resolved or rejected once all of the observed promises complete
60 | */
61 | waitForAll(promises:ng.IPromise[]):ng.IPromise {
62 | var deferred:ng.IDeferred = this.$q_.defer();
63 | var results:Object = {};
64 | var counter:number = 0;
65 | var errored:boolean = false;
66 |
67 | function updateResult(key:string, data:any):void {
68 | if (!results.hasOwnProperty(key)) {
69 | results[key] = data;
70 |
71 | counter--;
72 | }
73 |
74 | checkForDone();
75 | }
76 |
77 | function checkForDone():void {
78 | if (counter === 0) {
79 | if (errored) {
80 | deferred.reject(results);
81 | } else {
82 | deferred.resolve(results);
83 | }
84 | }
85 | }
86 |
87 | angular.forEach(promises, (promise:ng.IPromise, key:string) => {
88 | counter++;
89 |
90 | promise.then(
91 | (data:any) => {
92 | updateResult(key, data);
93 | },
94 | (data:any) => {
95 | errored = true;
96 |
97 | updateResult(key, data);
98 | });
99 | });
100 |
101 | checkForDone(); // Handle empty Array
102 |
103 | return deferred.promise;
104 | }
105 |
106 | /**
107 | * @inheritDoc
108 | */
109 | when(value:T):ng.IPromise {
110 | return this.$q_.when(value);
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/source/utils/string-util.ts:
--------------------------------------------------------------------------------
1 | module formFor {
2 |
3 | /**
4 | * Utility for working with strings.
5 | *
6 | * Intended for use only by formFor directive; this class is not exposed to the $injector.
7 | */
8 | export class StringUtil {
9 |
10 | /**
11 | * Converts text in common variable formats to humanized form.
12 | *
13 | * @param text Name of variable to be humanized (ex. myVariable, my_variable)
14 | * @returns Humanized string (ex. 'My Variable')
15 | */
16 | public static humanize(text:string):string {
17 | if (!text) {
18 | return '';
19 | }
20 |
21 | text = text.replace(/[A-Z]/g, function (match) {
22 | return ' ' + match;
23 | });
24 |
25 | text = text.replace(/_([a-z])/g, function (match, $1) {
26 | return ' ' + $1.toUpperCase();
27 | });
28 |
29 | text = text.replace(/\s+/g, ' ');
30 | text = text.trim();
31 | text = text.charAt(0).toUpperCase() + text.slice(1);
32 |
33 | return text;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/styles/default/_mixins.styl:
--------------------------------------------------------------------------------
1 | @require '_variables';
2 |
3 | /****** Vender prefixes ******/
4 |
5 | placeholder-color($color) {
6 | &::-webkit-input-placeholder {
7 | color: $color;
8 | }
9 | &:-moz-placeholder {
10 | /* FF 4-18 */
11 | color: $color;
12 | }
13 | &::-moz-placeholder {
14 | /* FF 19+ */
15 | color: $color;
16 | }
17 | &:-ms-input-placeholder {
18 | /* IE 10+ */
19 | color: $color;
20 | }
21 | }
22 |
23 | user-select(val) {
24 | -ms-user-select: val;
25 | -moz-user-select: val;
26 | -webkit-user-select: val;
27 | }
28 |
29 | appearance(val) {
30 | -ms-appearance: val;
31 | -moz-appearance: val;
32 | -webkit-appearance: val;
33 | }
34 |
35 | /****** Shared base classes ******/
36 |
37 | $form-for-input {
38 | display: block;
39 | box-sizing: border-box;
40 | width: 100%;
41 | line-height: 1.5;
42 | padding: 6px 9px;
43 |
44 | font-family: inherit;
45 | font-size: inherit;
46 | text-align: left;
47 | color: inherit;
48 |
49 | border: $border;
50 | border-radius: $border-radius;
51 |
52 | appearance: none;
53 | outline: none;
54 | }
55 |
56 | // Should be hidden from user but remain tabbable and clickable
57 | // Note that display:none and visibility:hidden prevent tab/mouse interaction
58 | $form-for-hidden-input {
59 | position: absolute;
60 | opacity: 0; // Hidden but clickable
61 | z-index: 2; // Float above the styled, faux-input so mouse clicks will toggle selected state
62 | width: 20px;
63 | height: 20px;
64 | cursor: pointer;
65 | margin: 0;
66 | }
67 |
68 | $form-for-toggle-ui {
69 | position: absolute;
70 | height: 20px;
71 | width: 20px;
72 | top: 0;
73 | left: 0;
74 | width: 19px;
75 | padding-left: 1px; // Visually centers better
76 | line-height: 19px;
77 | text-align: center;
78 | vertical-align: middle;
79 | display: inline-block;
80 | background-image: linear-gradient(to top, $color-fill-1, white);
81 | border: $border;
82 | position: relative;
83 | margin-right: 5px;
84 | cursor: pointer;
85 | vertical-align: middle;
86 | font-family: FontAwesome;
87 | }
88 |
--------------------------------------------------------------------------------
/styles/default/_variables.styl:
--------------------------------------------------------------------------------
1 | $color-fill-1 = #f9f9f9
2 | $color-fill-2 = #EFEFEF
3 | $color-fill-3 = #CCC
4 | $color-fill-4 = #AAA
5 | $color-fill-5 = #999
6 | $color-fill-6 = #666
7 |
8 | $color-text-1 = #848b88
9 | $color-text-2 = #3d3f3e
10 | $color-text-3 = #252726
11 |
12 | light-blue = #ebf6ff
13 | medium-blue = #66afe9
14 |
15 | blue = #0075ba
16 | gray = $color-fill-4
17 | green = #8eb32b
18 | orange = #ea9707
19 | purple = #813793
20 | red = #e35256
21 | teal = #00a99d
22 | yellow = #ead207
23 |
24 | $border-color = $color-fill-4
25 | $border = 1px solid $border-color
26 | $border-radius = 6px
27 | $border-color-focused = medium-blue
28 | $border-color-error = red
29 | $box-shadow-focused = 0 0 4px medium-blue;
30 | $box-shadow-error-focused = 0 0 4px red;
31 |
32 | $padding-small = 5px
33 |
34 | $opacity-disabled = .5
35 |
36 | $transition = all 100ms linear
37 |
38 | $box-shadow = inset 0 1px 1px rgba(0,0,0,.075)
39 |
--------------------------------------------------------------------------------
/styles/default/checkbox-field.styl:
--------------------------------------------------------------------------------
1 | @require '_mixins';
2 | @require '_variables';
3 |
4 | checkbox-field {
5 | display: block;
6 | position: relative;
7 | }
8 |
9 | .form-for-field {
10 | > input[type="checkbox"] {
11 | @extend $form-for-hidden-input
12 |
13 | +label {
14 | cursor: pointer;
15 | display: inline-block;
16 |
17 | &:before {
18 | @extend $form-for-toggle-ui
19 |
20 | border-radius: 4px;
21 | content: " "; // Hidden
22 | }
23 | }
24 |
25 | &:focus + label:before {
26 | border-color: $border-color-focused;
27 | box-shadow: $box-shadow-focused;
28 | }
29 |
30 | &:checked + label:before {
31 | content: "\f00c";
32 | }
33 |
34 | &:disabled {
35 | opacity: 0;
36 | cursor: default;
37 | pointer-events: none;
38 |
39 | +label {
40 | cursor: default;
41 | opacity: $opacity-disabled;
42 |
43 | &:before {
44 | cursor: default;
45 | }
46 | }
47 | }
48 | }
49 |
50 | > input[type="checkbox"]:invalid,
51 | &.invalid > input[type="checkbox"] {
52 | +label:before {
53 | border-color: red;
54 | }
55 |
56 | &:focus + label:before {
57 | box-shadow: $box-shadow-error-focused;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/styles/default/collection-label.styl:
--------------------------------------------------------------------------------
1 | .collection-label {
2 | position: relative;
3 | }
4 |
--------------------------------------------------------------------------------
/styles/default/field-error.styl:
--------------------------------------------------------------------------------
1 | @require '_variables';
2 |
3 | error-field-background-color = rgba(red, .95);
4 |
5 | .field-error {
6 | position: absolute;
7 | top: 100%;
8 | border-radius: 4px;
9 | background-color: red;
10 | background-color: error-field-background-color;
11 | color: white;
12 | padding: 7px 15px;
13 | z-index: 1000;
14 | margin: 0;
15 | pointer-events: none;
16 |
17 | &::before {
18 | content: '';
19 | border: 7px solid transparent;
20 | position: absolute;
21 | bottom: 100%;
22 | border-bottom-color: red;
23 | border-bottom-color: error-field-background-color;
24 | }
25 |
26 | &:not(.left-aligned) {
27 | right: 3px;
28 |
29 | &::before {
30 | right: 10px;
31 | }
32 | }
33 |
34 | &.left-aligned {
35 | left: 0;
36 |
37 | &::before {
38 | left: 4px;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/styles/default/field-label.styl:
--------------------------------------------------------------------------------
1 | @require '_variables';
2 |
3 | field-label {
4 | display: inline-block;
5 | }
6 |
7 | .field-label {
8 | position: relative;
9 | cursor: inherit;
10 | font-weight: bold;
11 | user-select: none;
12 |
13 | &:hover {
14 | .form-for-tooltip {
15 | .form-for-tooltip-popover {
16 | display: inline-block;
17 | }
18 | }
19 | }
20 | }
21 |
22 | .field-label-required-label {
23 | color: $color-text-1;
24 | font-style: italic;
25 | font-size: .8em;
26 | }
27 |
--------------------------------------------------------------------------------
/styles/default/form-for-field.styl:
--------------------------------------------------------------------------------
1 | .form-for-field {
2 | display: block;
3 | position: relative;
4 | }
5 |
--------------------------------------------------------------------------------
/styles/default/form-for.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bvaughn/angular-form-for/1f67ffb512e11866572ccc094e77567761c76aaf/styles/default/form-for.styl
--------------------------------------------------------------------------------
/styles/default/radio-field.styl:
--------------------------------------------------------------------------------
1 | @require '_mixins';
2 | @require '_variables';
3 |
4 | radio-field {
5 | display: block;
6 | position: relative;
7 | }
8 |
9 | .form-for-field {
10 | > label {
11 | display: block;
12 |
13 | > input[type="radio"] {
14 | @extend $form-for-hidden-input
15 |
16 | +span {
17 | cursor: pointer;
18 | display: inline-block;
19 |
20 | &:before {
21 | @extend $form-for-toggle-ui
22 |
23 | border-radius: 10px;
24 | content: " "; // Hidden
25 | font-size: 11px;
26 | }
27 | }
28 |
29 | &:focus + span:before {
30 | border-color: $border-color-focused;
31 | box-shadow: $box-shadow-focused;
32 | }
33 |
34 | &:checked + span:before {
35 | content: "\f111";
36 | }
37 |
38 | &:disabled {
39 | opacity: 0;
40 | cursor: default;
41 | pointer-events: none;
42 |
43 | +span {
44 | cursor: default;
45 | opacity: $opacity-disabled;
46 |
47 | &:before {
48 | cursor: default;
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | > label > input[type="radio"]:invalid,
56 | &.invalid > label > input[type="radio"] {
57 | +span:before {
58 | border-color: red;
59 | }
60 |
61 | &:focus + span:before {
62 | box-shadow: $box-shadow-error-focused;
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/styles/default/select-field.styl:
--------------------------------------------------------------------------------
1 | @require '_mixins';
2 | @require '_variables';
3 |
4 | select-field {
5 | display: block;
6 | }
7 |
8 | /************************* Select *************************/
9 |
10 | .form-for-field {
11 | select {
12 | @extend $form-for-input
13 |
14 | background: linear-gradient(top, white, $color-fill-2);
15 | padding-right: 30px;
16 |
17 | &:focus {
18 | background: white;
19 | outline: none;
20 | border-color: $border-color-focused;
21 | box-shadow: $box-shadow-focused;
22 | }
23 |
24 | &:disabled {
25 | opacity: $opacity-disabled;
26 | pointer-events: none;
27 |
28 | +[c-field--select-arrows] {
29 | opacity: $opacity-disabled;
30 | }
31 | }
32 | }
33 |
34 | &.invalid select {
35 | border-color: red;
36 |
37 | &:focus {
38 | box-shadow: $box-shadow-error-focused;
39 | }
40 | }
41 |
42 | .form-for-select-arrows:after {
43 | display: block;
44 | position: absolute;
45 | right: 5px;
46 | bottom: 0;
47 | width: 20px;
48 | height: 35px;
49 | line-height: 35px;
50 | text-align: center;
51 | font-style: normal;
52 | content: "\f0dc";
53 | font-family: FontAwesome;
54 | color: $color-text-1;
55 | pointer-events: none;
56 | }
57 | }
58 |
59 | @-moz-document url-prefix() {
60 | .form-for-select-arrows {
61 | display: none; // Fix Firefox double arrows
62 | }
63 | }
64 |
65 | /************************* Typeahead *************************/
66 |
67 | .form-for-field {
68 | input[type="search"] {
69 | @extend $form-for-input
70 |
71 | box-shadow: inset 0 1px 4px rgba(0,0,0, 0.15);
72 |
73 | +ul {
74 | display: none; // Hidden until focused
75 | position: absolute;
76 | top: 100%;
77 | left: 0;
78 | width: 100%;
79 | z-index: 4; // Needs to be higher than .has-error
80 | padding: 0;
81 | margin: 0;
82 | list-style: none;
83 | background-color: white;
84 | border: $border;
85 | border-color: $border-color-focused;
86 | border-radius: $border-radius;
87 | border-top: none;
88 | border-top-left-radius: 0;
89 | border-top-right-radius: 0;
90 | box-sizing: border-box;
91 | overflow: auto;
92 | }
93 |
94 | &:focus {
95 | border-color: $border-color-focused;
96 | border-bottom-left-radius: 0;
97 | border-bottom-right-radius: 0;
98 | box-shadow: $box-shadow-focused;
99 |
100 | +ul {
101 | display: block;
102 |
103 | li {
104 | padding: 6px 9px;
105 |
106 | strong {
107 | font-weight: inherit;
108 | }
109 |
110 | &.form-for-typeahead-list-item-active {
111 | font-weight: 600;
112 | }
113 |
114 | &.form-for-typeahead-list-item-hover,
115 | &:hover {
116 | background-color: light-blue;
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
123 | &.invalid input[type="search"],
124 | input[type="search"]:invalid {
125 | border-color: red;
126 |
127 | +ul {
128 | border-color: red;
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/styles/default/submit-button.styl:
--------------------------------------------------------------------------------
1 | @import 'nib';
2 | @require '_variables';
3 |
4 | submit-field {
5 | display: block;
6 | }
7 |
8 | .submit-button {
9 | user-select: none;
10 |
11 | transition: $transition;
12 |
13 | border-radius: $border-radius;
14 | border-style: none;
15 | background-color: blue;
16 | color: white;
17 | cursor: pointer;
18 | display: inline-block;
19 | font-size: inherit;
20 | font-weight: 400;
21 | line-height: 35px;
22 | outline: 0;
23 | padding: 0 25px;
24 | white-space: nowrap;
25 |
26 | &:not(.disabled):not(:disabled) {
27 | &:hover, &:active, &:focus {
28 | background-color: lighten(blue, 5%);
29 | color: white;
30 | }
31 | }
32 |
33 | &:active {
34 | transition: none;
35 |
36 | box-shadow: inset 0 4px 2px rgba(0,0,0, 0.1);
37 | }
38 |
39 | &.disabled,
40 | &:disabled {
41 | opacity: .5;
42 | cursor: default;
43 | box-shadow: none;
44 | }
45 | }
46 |
47 | .submit-button-icon {
48 | margin-right: 5px;
49 | }
50 |
--------------------------------------------------------------------------------
/styles/default/text-field.styl:
--------------------------------------------------------------------------------
1 | @require '_mixins';
2 | @require '_variables';
3 |
4 | text-field {
5 | display: block;
6 | }
7 |
8 | .form-for-field {
9 | input,
10 | textarea {
11 |
12 | @extend $form-for-input
13 |
14 | box-shadow: inset 0 1px 4px rgba(0,0,0, 0.15);
15 |
16 | &:focus {
17 | border-color: $border-color-focused;
18 | box-shadow: $box-shadow-focused;
19 | }
20 |
21 | &:disabled {
22 | opacity: $opacity-disabled;
23 | pointer-events: none;
24 | }
25 | }
26 |
27 | &.invalid input,
28 | &.invalid textarea,
29 | input:invalid
30 | textarea:invalid {
31 | border-color: red;
32 |
33 | &:focus {
34 | box-shadow: $box-shadow-error-focused;
35 | }
36 | }
37 |
38 | &.with-icon-before {
39 | input,
40 | textarea {
41 | padding-left: 40px;
42 | }
43 | }
44 |
45 | &.with-icon-after {
46 | input,
47 | textarea {
48 | padding-right: 40px;
49 | }
50 | }
51 | }
52 |
53 | .form-for-input-icon-left,
54 | .form-for-input-icon-right {
55 | position: absolute;
56 | bottom: 17px;
57 | width: 40px;
58 | text-align: center;
59 | line-height: 1em;
60 | margin-bottom: -.5em;
61 | }
62 |
63 | .form-for-input-icon-left {
64 | left: 0;
65 | }
66 |
67 | .form-for-input-icon-right {
68 | right: 0;
69 | }
70 |
--------------------------------------------------------------------------------
/styles/default/tooltip.styl:
--------------------------------------------------------------------------------
1 | @require '_variables';
2 |
3 | .form-for-tooltip {
4 | display: inline-flex;
5 | align-items: center;
6 | height: 15px;
7 | width: 15px;
8 | }
9 |
10 | .form-for-tooltip-icon {
11 | display: inline-block;
12 | width: 15px;
13 | height: 15px;
14 | flex: 0 0 15px;
15 | border-radius: 15px;
16 | line-height: 15px;
17 | background-color: $color-text-1;
18 | color: white;
19 | text-align: center;
20 | font-weight: 600;
21 | font-size: 12px;
22 | cursor: default;
23 |
24 | &:hover {
25 | background-color: $color-text-2;
26 | }
27 | }
28 |
29 | .form-for-tooltip-popover {
30 | pointer-events: none;
31 | display: none;
32 | position: relative;
33 | z-index: 2;
34 | flex: 0 0 350px;
35 | width: 350px;
36 | border-radius: $border-radius;
37 | padding: 15px;
38 | margin-left: 15px;
39 | background-color: light-blue;
40 | color: $color-text-3;
41 | border-bottom: 1px solid rgba(0,0,0,.15);
42 | font-weight: 400;
43 |
44 | &:before {
45 | position: absolute;
46 | right: 100%;
47 | top: 50%;
48 | margin-top: -10px;
49 | content: '';
50 | border: 10px solid transparent;
51 | border-right-color: light-blue;
52 | color: $color-text-3;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/styles/default/type-ahead-field.styl:
--------------------------------------------------------------------------------
1 | @require '_mixins';
2 | @require '_variables';
3 |
4 | type-ahead-field {
5 | display: block;
6 | }
--------------------------------------------------------------------------------
/styles/material/field-error.styl:
--------------------------------------------------------------------------------
1 | field-error {
2 | display: block;
3 |
4 | // formFor fieldError isn't nested inside of an mdInputContainer and so doesn't inherit the styles.
5 | // These are the relevant ones though...
6 | .text-danger {
7 | font-size: 12px;
8 | line-height: 24px;
9 | color: rgb(244, 67, 54);
10 | }
11 | }
--------------------------------------------------------------------------------
/styles/material/md-checkbox.styl:
--------------------------------------------------------------------------------
1 | md-checkbox {
2 | display: inline-block !important;
3 | }
--------------------------------------------------------------------------------
/styles/material/md-input-container.styl:
--------------------------------------------------------------------------------
1 | md-input-container {
2 | }
--------------------------------------------------------------------------------
/styles/material/select-field.styl:
--------------------------------------------------------------------------------
1 | select-field {
2 | md-select {
3 | padding: 0;
4 | }
5 |
6 | md-input-container {
7 | label {
8 | display: none;
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/templates/bootstrap/checkbox-field.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
17 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/templates/bootstrap/collection-label.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/bootstrap/field-error.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/templates/bootstrap/field-label.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/templates/bootstrap/radio-field.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/templates/bootstrap/select-field.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
14 |
16 |
17 |
18 |
21 |
22 |
25 |
26 |
29 |
30 |
--------------------------------------------------------------------------------
/templates/bootstrap/select-field/_multi-select.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/templates/bootstrap/select-field/_select.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/templates/bootstrap/submit-button.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/bootstrap/text-field.html:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/templates/bootstrap/text-field/_input.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/templates/bootstrap/text-field/_textarea.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/templates/bootstrap/type-ahead-field.html:
--------------------------------------------------------------------------------
1 |
50 |
--------------------------------------------------------------------------------
/templates/default/checkbox-field.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
16 |
17 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/templates/default/collection-label.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/default/field-error.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/templates/default/field-label.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 | ?
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/templates/default/radio-field.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/templates/default/select-field.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
20 |
21 |
24 |
25 |
--------------------------------------------------------------------------------
/templates/default/select-field/_multi-select.html:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/templates/default/select-field/_select.html:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/templates/default/submit-button.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/templates/default/text-field.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
24 |
25 |
--------------------------------------------------------------------------------
/templates/default/text-field/_input.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/templates/default/text-field/_textarea.html:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/templates/default/type-ahead-field.html:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/templates/material/checkbox-field.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{label}}
8 |
9 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/templates/material/collection-label.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/material/field-error.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/templates/material/field-label.html:
--------------------------------------------------------------------------------
1 |
2 |
4 | {{help}}
5 |
6 |
--------------------------------------------------------------------------------
/templates/material/radio-field.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/templates/material/select-field.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
12 |
13 |
15 | {{option[labelAttribute]}}
16 |
17 |
18 |
19 |
24 |
25 |
27 | {{option[labelAttribute]}}
28 |
29 |
30 |
31 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/templates/material/submit-button.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/templates/material/text-field.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
18 |
19 |
31 |
32 |
35 |
--------------------------------------------------------------------------------
/templates/material/type-ahead-field.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
20 |
21 |
22 |
23 | {{option[labelAttribute]}}
24 |
25 |
26 |
27 |
28 | No matches found for "{{scopeBuster.filter}}".
29 |
30 |
31 |
32 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/integration/directives/checkbox-field.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var testHelper = require('../test-helper');
4 |
5 | // TODO Test field-errors
6 |
7 | // Interface between tests (below) and the appropriate formFor Checkbox directive template
8 | var Facade = function(identifier) {
9 | testHelper.goToPage('http://localhost:8000/examples/checkbox-field.html?template=' + identifier);
10 |
11 | var fieldName;
12 |
13 | this.setFieldName = function(value) {
14 | fieldName = value;
15 | };
16 |
17 | this.getCheckbox = function() {
18 | return element(by.css('[attribute=' + fieldName + ']'));
19 | };
20 |
21 | switch (identifier) {
22 | case 'bootstrap':
23 | this.getClickable = function() {
24 | return element(by.css('[attribute=' + fieldName + '] input'));
25 | };
26 | this.getErrorText = function() {
27 | return element(by.css('[attribute=' + fieldName + '] field-error p'));
28 | };
29 | this.getHoverable = function() {
30 | return element(by.css('[attribute=' + fieldName + '] [popover]'));
31 | };
32 | this.getTooltip = function() {
33 | return element(by.css('[attribute=' + fieldName + '] [popover-popup]'));
34 | };
35 | break;
36 | case 'default':
37 | this.getClickable = function() {
38 | return element(by.css('[attribute=' + fieldName + '] label'));
39 | };
40 | this.getErrorText = function() {
41 | return element(by.css('[attribute=' + fieldName + '] field-error p'));
42 | };
43 | this.getHoverable = function() {
44 | return element(by.css('[attribute=' + fieldName + '] .form-for-tooltip-icon'));
45 | };
46 | this.getTooltip = function() {
47 | return element(by.css('[attribute=' + fieldName + '] .form-for-tooltip-popover'));
48 | };
49 | break;
50 | case 'material':
51 | browser.ignoreSynchronization = true;
52 |
53 | this.getClickable = function() {
54 | return element(by.css('[attribute=' + fieldName + '] md-checkbox'));
55 | };
56 | this.getErrorText = function() {
57 | return element(by.css('[attribute=' + fieldName + '] field-error div'));
58 | };
59 | this.getHoverable = function() {
60 | return element(by.css('[attribute=' + fieldName + '] label'));
61 | };
62 | this.getTooltip = function() {
63 | return element(by.css('md-tooltip'));
64 | };
65 | break;
66 | }
67 | };
68 |
69 | // Test each of our templates
70 | [
71 | 'bootstrap',
72 | 'default',
73 | 'material'
74 | ].forEach(function(template) {
75 | describe(template, function() {
76 | var checkbox, clickable, facade, hoverable, tooltip;
77 |
78 | beforeEach(function() {
79 | facade = new Facade(template);
80 |
81 | browser.driver.manage().window().setSize(1600, 1000);
82 | });
83 |
84 | describe('enabled', function() {
85 | beforeEach(function() {
86 | facade.setFieldName('enabled');
87 |
88 | checkbox = facade.getCheckbox();
89 | clickable = facade.getClickable();
90 | });
91 |
92 | it('should show the correct label', function () {
93 | expect(checkbox.getText()).toBe('Checkbox checkbox');
94 | });
95 |
96 | it('should not show an error', function() {
97 | testHelper.assertIsNotDisplayed(facade.getErrorText());
98 | });
99 |
100 | it('should update the model on click', function () {
101 | expect(clickable.evaluate('model.bindable')).toBeFalsy();
102 |
103 | clickable.click();
104 |
105 | expect(clickable.evaluate('model.bindable')).toBeTruthy();
106 | });
107 | });
108 |
109 | describe('preselected', function() {
110 | beforeEach(function() {
111 | facade.setFieldName('preselected');
112 |
113 | checkbox = facade.getCheckbox();
114 | clickable = facade.getClickable();
115 | });
116 |
117 | it('should show the correct label', function () {
118 | expect(checkbox.getText()).toBe('Preselected checkbox');
119 | });
120 |
121 | it('should update the model on click', function() {
122 | expect(clickable.evaluate('model.bindable')).toBeTruthy();
123 |
124 | clickable.click();
125 |
126 | expect(clickable.evaluate('model.bindable')).toBeFalsy();
127 | });
128 | });
129 |
130 | describe('help', function() {
131 | beforeEach(function() {
132 | facade.setFieldName('help');
133 | });
134 |
135 | it('should not show help text by default', function() {
136 | if (template === 'material') return; // Material template doesn't expose a help tooltip
137 |
138 | testHelper.assertIsNotDisplayed(facade.getTooltip());
139 | });
140 |
141 | it('should show help text on hover', function() {
142 | if (template === 'material') return; // Material templates don't have help icons
143 |
144 | testHelper.doMouseOver(facade.getHoverable());
145 | testHelper.assertIsDisplayed(facade.getTooltip());
146 | });
147 | });
148 |
149 | describe('disabled', function() {
150 | beforeEach(function() {
151 | facade.setFieldName('disabled');
152 |
153 | checkbox = facade.getCheckbox();
154 | clickable = facade.getClickable();
155 | hoverable = facade.getHoverable();
156 | tooltip = facade.getTooltip();
157 | });
158 |
159 | it('should show the correct label', function () {
160 | expect(checkbox.getText()).toBe('Disabled checkbox');
161 | });
162 |
163 | it('should be disabled based on html attributes', function () {
164 | expect(checkbox.getAttribute('disable')).toBe('true');
165 | });
166 |
167 | it('should not update the model on click', function () {
168 | expect(clickable.evaluate('model.bindable')).toBeFalsy();
169 |
170 | clickable.click();
171 |
172 | expect(clickable.evaluate('model.bindable')).toBeFalsy();
173 | });
174 | });
175 |
176 | describe('invalid', function() {
177 | it('should show an error', function () {
178 | facade.setFieldName('invalid');
179 |
180 | testHelper.assertIsDisplayed(facade.getErrorText());
181 | });
182 | });
183 | });
184 | });
--------------------------------------------------------------------------------
/tests/integration/directives/radio-field.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var testHelper = require('../test-helper');
4 |
5 | // Interface between tests (below) and the appropriate formFor Radio directive template
6 | var Facade = function(identifier) {
7 | testHelper.goToPage('http://localhost:8000/examples/radio-field.html?template=' + identifier);
8 |
9 | var fieldName;
10 |
11 | this.setFieldName = function(value) {
12 | fieldName = value;
13 | };
14 |
15 | this.getRadio = function() {
16 | return element(by.css('[attribute=' + fieldName + ']'));
17 | };
18 |
19 | switch (identifier) {
20 | case 'bootstrap':
21 | this.getInputs = function() {
22 | return element.all(by.css('[attribute=' + fieldName + '] [ng-repeat] input'));
23 | };
24 | this.getLabels = function() {
25 | return element.all(by.css('[attribute=' + fieldName + '] [ng-repeat] label'));
26 | };
27 | this.getErrorText = function() {
28 | return element(by.css('[attribute=' + fieldName + '] field-error p'));
29 | };
30 | this.getGroupLabel = function() {
31 | return element(by.css('[attribute=' + fieldName + '] label [ng-bind-html]'));
32 | };
33 | this.getHoverable = function() {
34 | return element(by.css('[attribute=' + fieldName + '] label [popover]'));
35 | };
36 | this.getTooltip = function() {
37 | return element(by.css('[attribute=' + fieldName + '] label [popover-popup]'));
38 | };
39 | break;
40 | case 'default':
41 | this.getInputs = function() {
42 | return element.all(by.css('[attribute=' + fieldName + '] [ng-repeat] input'));
43 | };
44 | this.getLabels = function() {
45 | return element.all(by.css('[attribute=' + fieldName + '] [ng-repeat]'));
46 | };
47 | this.getErrorText = function() {
48 | return element(by.css('[attribute=' + fieldName + '] field-error p'));
49 | };
50 | this.getGroupLabel = function() {
51 | return element(by.css('[attribute=' + fieldName + '] label [ng-bind-html]'));
52 | };
53 | this.getHoverable = function() {
54 | return element(by.css('[attribute=' + fieldName + '] label .form-for-tooltip'));
55 | };
56 | this.getTooltip = function() {
57 | return element(by.css('[attribute=' + fieldName + '] label .form-for-tooltip-popover'));
58 | };
59 | break;
60 | case 'material':
61 | browser.ignoreSynchronization = true;
62 |
63 | this.getInputs = function() {
64 | return element.all(by.css('[attribute=' + fieldName + '] md-radio-group md-radio-button'));
65 | };
66 | this.getLabels = function() {
67 | return element.all(by.css('[attribute=' + fieldName + '] md-radio-group md-radio-button'));
68 | };
69 | this.getErrorText = function() {
70 | return element(by.css('[attribute=' + fieldName + '] field-error div'));
71 | };
72 | this.getGroupLabel = function() {
73 | return element(by.css('[attribute=' + fieldName + '] md-radio-group label'));
74 | };
75 | this.getHoverable = function() {
76 | return element(by.css('[attribute=' + fieldName + '] md-radio-group label'));
77 | };
78 | this.getTooltip = function() {
79 | return element(by.css('md-tooltip'));
80 | };
81 | break;
82 | }
83 | };
84 |
85 | // Test each of our templates
86 | [
87 | 'bootstrap',
88 | 'default',
89 | 'material'
90 | ].forEach(function(template) {
91 | describe(template, function() {
92 | var radio, groupLabel, femaleInput, femaleLabel, maleInput, maleLabel, facade, hoverable, tooltip;
93 |
94 | beforeEach(function() {
95 | facade = new Facade(template);
96 |
97 | browser.driver.manage().window().setSize(1600, 1000);
98 | });
99 |
100 | describe('preselected', function() {
101 | beforeEach(function() {
102 | facade.setFieldName('preselected');
103 |
104 | radio = facade.getRadio();
105 | groupLabel = facade.getGroupLabel();
106 | femaleLabel = facade.getLabels().get(0);
107 | maleLabel = facade.getLabels().get(1);
108 | });
109 |
110 | it('should show the correct label', function () {
111 | expect(groupLabel.getText()).toBe('Preselected radio');
112 | });
113 |
114 | it('should not show an error', function() {
115 | testHelper.assertIsNotDisplayed(facade.getErrorText());
116 | });
117 |
118 | it('should show the correct labels', function() {
119 | expect(femaleLabel.getText()).toBe('Female');
120 | expect(maleLabel.getText()).toBe('Male');
121 | });
122 |
123 | it('should preselect the correct initial value', function() {
124 | expect(femaleLabel.evaluate('model.bindable')).toBe('f');
125 | expect(maleLabel.evaluate('model.bindable')).toBe('f');
126 | });
127 |
128 | it('should update the model on click', function() {
129 | maleLabel.click();
130 |
131 | expect(radio.evaluate('formData.preselected')).toBe('m');
132 |
133 | femaleLabel.click();
134 |
135 | expect(radio.evaluate('formData.preselected')).toBe('f');
136 | });
137 | });
138 |
139 | describe('help', function() {
140 | beforeEach(function() {
141 | facade.setFieldName('help');
142 |
143 | hoverable = facade.getHoverable();
144 | tooltip = facade.getTooltip();
145 | });
146 |
147 | it('should not show help text by default', function() {
148 | if (template === 'material') return; // Material template doesn't expose a help tooltip
149 |
150 | testHelper.assertIsNotDisplayed(facade.getTooltip());
151 | });
152 |
153 | it('should show help text on hover', function() {
154 | if (template === 'material') return; // Material template doesn't expose a help tooltip
155 |
156 | testHelper.doMouseOver(facade.getHoverable());
157 | testHelper.assertIsDisplayed(facade.getTooltip());
158 | });
159 | });
160 |
161 | describe('disabled', function() {
162 | beforeEach(function() {
163 | facade.setFieldName('disabled');
164 |
165 | radio = facade.getRadio();
166 | groupLabel = facade.getGroupLabel();
167 | femaleInput = facade.getInputs().get(0);
168 | femaleLabel = facade.getLabels().get(0);
169 | maleInput = facade.getInputs().get(1);
170 | maleLabel = facade.getLabels().get(1);
171 | });
172 |
173 | it('should show the correct label', function () {
174 | expect(groupLabel.getText()).toBe('Disabled radio');
175 | });
176 |
177 | it('should be disabled based on html attributes', function () {
178 | expect(femaleInput.getAttribute('disabled')).toBe('true');
179 | expect(maleInput.getAttribute('disabled')).toBe('true');
180 | });
181 |
182 | it('should not update the model on click', function () {
183 | expect(radio.evaluate('model.bindable')).toBeFalsy();
184 |
185 | femaleInput.click().then(
186 | function() {},
187 | function() {}
188 | );
189 | maleInput.click().then(
190 | function() {},
191 | function() {}
192 | );
193 |
194 | expect(radio.evaluate('model.bindable')).toBeFalsy();
195 | });
196 | });
197 |
198 | describe('invalid', function() {
199 | it('should show an error', function () {
200 | facade.setFieldName('invalid');
201 |
202 | testHelper.assertIsDisplayed(facade.getErrorText(), 500);
203 | });
204 | });
205 | });
206 | });
--------------------------------------------------------------------------------
/tests/integration/test-helper.js:
--------------------------------------------------------------------------------
1 | exports.assertElementIsClickable = function(element, opt_timeout) {
2 | var promise = protractor.ExpectedConditions.elementToBeClickable(element);
3 |
4 | return browser.wait(promise, opt_timeout || 100);
5 | };
6 |
7 | exports.assertElementIsNotClickable = function(element, opt_timeout) {
8 | return exports.assertElementIsClickable(element, opt_timeout).then(
9 | function() {
10 | throw Error('Element should not be clickable');
11 | },
12 | function() {
13 | // An element that is not clickable is what we want
14 | });
15 | };
16 |
17 | exports.assertFormDataValue = function(fieldName, expectedValue) {
18 | expect(exports.getFormDataValue(fieldName)).toBe(expectedValue);
19 | };
20 |
21 | exports.assertIsDisplayed = function(element, opt_timeout) {
22 | browser.wait(function () {
23 | return element.isPresent() &&
24 | element.isDisplayed();
25 | }, opt_timeout || 100);
26 | };
27 |
28 | exports.assertIsNotDisplayed = function(element) {
29 | return element.isDisplayed().then(
30 | function(isDisplayed) {
31 | if (isDisplayed) {
32 | throw Error('Element should not be displayed');
33 | }
34 | },
35 | function() {
36 | // An element not present in the DOM is not displayed (which is okay)
37 | });
38 | };
39 |
40 | exports.doMouseOver = function(element) {
41 | browser.actions().mouseMove(element).perform();
42 | };
43 |
44 | exports.getFormDataValue = function(fieldName) {
45 | return element(by.css('form')).evaluate('formData.' + fieldName);
46 | };
47 |
48 | exports.goToPage = function(url) {
49 | browser.driver.get(url);
50 | browser.driver.wait(browser.driver.isElementPresent(by.id('form')), 5000);
51 | };
--------------------------------------------------------------------------------
/tests/unit/services/form-for-state-helper.js:
--------------------------------------------------------------------------------
1 | describe('$FormForStateHelper', function() {
2 | 'use strict';
3 |
4 | beforeEach(module('formFor'));
5 |
6 | var formForStateHelper;
7 |
8 | beforeEach(inject(function ($injector) {
9 | var $parse = $injector.get('$parse');
10 |
11 | formForStateHelper = new formFor.FormForStateHelper($parse, {});
12 | }));
13 |
14 | describe('get/set field errors', function() {
15 | it('should read and write to shallow objects', function() {
16 | formForStateHelper.setFieldError('shallow', 'error');
17 |
18 | expect(formForStateHelper.getFieldError('shallow')).toMatch('error');
19 | });
20 |
21 | it('should read and write to deep objects', function() {
22 | formForStateHelper.setFieldError('nested.path', 'error');
23 |
24 | expect(formForStateHelper.getFieldError('nested.path')).toMatch('error');
25 | });
26 | });
27 |
28 | describe('get/set field modifications', function() {
29 | it('should read and write to shallow objects', function() {
30 | expect(formForStateHelper.hasFieldBeenModified('shallow')).toBeFalsy();
31 |
32 | formForStateHelper.setFieldHasBeenModified('shallow', true);
33 |
34 | expect(formForStateHelper.hasFieldBeenModified('shallow')).toBeTruthy();
35 | });
36 |
37 | it('should read and write to deep objects', function() {
38 | expect(formForStateHelper.hasFieldBeenModified('nested.path')).toBeFalsy();
39 |
40 | formForStateHelper.setFieldHasBeenModified('nested.path', true);
41 |
42 | expect(formForStateHelper.hasFieldBeenModified('nested.path')).toBeTruthy();
43 | });
44 |
45 | it('should reset when pristine', function() {
46 | formForStateHelper.setFieldHasBeenModified('shallow', true);
47 |
48 | expect(formForStateHelper.hasFieldBeenModified('shallow')).toBeTruthy();
49 |
50 | formForStateHelper.setFieldHasBeenModified('shallow', false);
51 |
52 | expect(formForStateHelper.hasFieldBeenModified('shallow')).toBeFalsy();
53 | });
54 | });
55 |
56 | describe('get/set form submitted', function() {
57 | it('should read and write to shallow objects', function() {
58 | expect(formForStateHelper.hasFormBeenSubmitted()).toBeFalsy();
59 |
60 | formForStateHelper.setFormSubmitted(true);
61 |
62 | expect(formForStateHelper.hasFormBeenSubmitted()).toBeTruthy();
63 | });
64 |
65 | it('should reset when pristine', function() {
66 | formForStateHelper.setFormSubmitted(true);
67 |
68 | expect(formForStateHelper.hasFormBeenSubmitted()).toBeTruthy();
69 |
70 | formForStateHelper.setFormSubmitted(false);
71 |
72 | expect(formForStateHelper.hasFormBeenSubmitted()).toBeFalsy();
73 | });
74 | });
75 |
76 | describe('get field/form validity', function() {
77 | it('should report form as invalid if and only if it contains fields with errors', function() {
78 | expect(formForStateHelper.isFormInvalid()).toBeFalsy();
79 | expect(formForStateHelper.isFormValid()).toBeTruthy();
80 |
81 | formForStateHelper.setFieldError('nested.path', 'error');
82 | formForStateHelper.setFieldError('shallow', 'error');
83 |
84 | expect(formForStateHelper.isFormInvalid()).toBeTruthy();
85 | expect(formForStateHelper.isFormValid()).toBeFalsy();
86 |
87 | formForStateHelper.setFieldError('nested.path', null);
88 | formForStateHelper.setFieldError('shallow', null);
89 |
90 | expect(formForStateHelper.isFormInvalid()).toBeFalsy();
91 | expect(formForStateHelper.isFormValid()).toBeTruthy();
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/tests/unit/services/nested-object-helper.js:
--------------------------------------------------------------------------------
1 | describe('NestedObjectHelper', function() {
2 | 'use strict';
3 |
4 | beforeEach(module('formFor'));
5 |
6 | var nestedObjectHelper;
7 |
8 | beforeEach(inject(function ($injector) {
9 | var $parse = $injector.get('$parse');
10 |
11 | nestedObjectHelper = new formFor.NestedObjectHelper($parse);
12 | }));
13 |
14 | describe('flattenAttribute', function() {
15 | it('should strip dot notation syntax from a string containing it', function() {
16 | expect(nestedObjectHelper.flattenAttribute('foo.bar.baz')).not.toMatch(/\./);
17 | });
18 |
19 | it('should not adjust a string without dot notation', function() {
20 | expect(nestedObjectHelper.flattenAttribute('foo')).toMatch('foo');
21 | });
22 |
23 | it('should handle array notation', function() {
24 | expect(nestedObjectHelper.flattenAttribute('foo[1].bar')).toMatch('foo___1___bar');
25 | });
26 | });
27 |
28 | describe('flattenObjectKeys', function() {
29 | it('should iterate over all of the keys in a shallow object', function() {
30 | var keys = nestedObjectHelper.flattenObjectKeys({
31 | foo: 1,
32 | bar: 'two',
33 | baz: true
34 | });
35 |
36 | expect(keys).toContain('foo');
37 | expect(keys).toContain('bar');
38 | expect(keys).toContain('baz');
39 | });
40 |
41 | it('should iterate over all of the keys in a deep object', function() {
42 | var keys = nestedObjectHelper.flattenObjectKeys({
43 | foo: 1,
44 | deep: {
45 | bar: 'two',
46 | deeper: {
47 | baz: true
48 | }
49 | }
50 | });
51 |
52 | expect(keys).toContain('foo');
53 | expect(keys).toContain('deep.bar');
54 | expect(keys).toContain('deep.deeper.baz');
55 | });
56 |
57 | it('should iterate over items in an array', function() {
58 | var keys = nestedObjectHelper.flattenObjectKeys({
59 | foo: [
60 | 'string',
61 | {
62 | bar: true,
63 | baz: 'yes'
64 | }
65 | ]
66 | });
67 |
68 | expect(keys).toContain('foo[0]');
69 | expect(keys).toContain('foo[1]');
70 | expect(keys).toContain('foo[1].bar');
71 | expect(keys).toContain('foo[1].baz');
72 | });
73 | });
74 |
75 | describe('readAttribute', function() {
76 | var object = {
77 | foo: 123,
78 | bar: {
79 | baz: 456
80 | }
81 | };
82 |
83 | it('should read shallow attributes', function() {
84 | expect(nestedObjectHelper.readAttribute(object, 'foo')).toBe(123);
85 | });
86 |
87 | it('should read deep attributes using dot notation', function() {
88 | expect(nestedObjectHelper.readAttribute(object, 'bar.baz')).toBe(456);
89 | });
90 |
91 | it('should handle attributes that are missing', function() {
92 | expect(nestedObjectHelper.readAttribute(object, 'fake')).toBeFalsy();
93 | });
94 |
95 | it('should handle array notation when array is empty or non-existent', function() {
96 | object = {
97 | empty: []
98 | };
99 |
100 | expect(nestedObjectHelper.readAttribute(object, 'empty[0]')).toBeFalsy();
101 | expect(nestedObjectHelper.readAttribute(object, 'nonexistent[0]')).toBeFalsy();
102 | });
103 |
104 | it('should handle array notation by reading values from an array at the specified index', function() {
105 | object = {
106 | array: ['one']
107 | };
108 |
109 | expect(nestedObjectHelper.readAttribute(object, 'array[0]')).toMatch('one');
110 | });
111 | });
112 |
113 | describe('writeAttribute', function() {
114 | var object = {
115 | foo: 123,
116 | bar: {
117 | baz: 456
118 | }
119 | };
120 |
121 | it('should write shallow attributes', function() {
122 | nestedObjectHelper.writeAttribute(object, 'foo', 'new value');
123 |
124 | expect(object.foo).toMatch('new value');
125 | });
126 |
127 | it('should write deep attributes using dot notation', function() {
128 | nestedObjectHelper.writeAttribute(object, 'bar.baz', 'woohoo');
129 |
130 | expect(object.bar.baz).toMatch('woohoo');
131 | });
132 |
133 | it('should handle attributes that are missing', function() {
134 | nestedObjectHelper.writeAttribute(object, 'nonexistent', 'brand new');
135 |
136 | expect(object.nonexistent).toMatch('brand new');
137 | });
138 |
139 | it('should handle array notation by creating arrays that do not yet exist', function() {
140 | nestedObjectHelper.writeAttribute(object, 'collection[0]', 'first item');
141 |
142 | expect(object.collection).toBeTruthy();
143 | expect(object.collection[0]).toMatch('first item');
144 | });
145 |
146 | it('should handle array notation by creating indexes that do not yet exist', function() {
147 | object.collection = ['one'];
148 |
149 | nestedObjectHelper.writeAttribute(object, 'collection[1]', 'two');
150 |
151 | expect(object.collection).toBeTruthy();
152 | expect(object.collection[0]).toMatch('one');
153 | expect(object.collection[1]).toMatch('two');
154 | });
155 |
156 | it('should handle array notation with nested objects for indexes that do not yet exist', function() {
157 | object.collection = [];
158 |
159 | nestedObjectHelper.writeAttribute(object, 'collection[0].number', 'one');
160 |
161 | expect(object.collection).toBeTruthy();
162 | expect(object.collection[0].number).toMatch('one');
163 | });
164 |
165 | it('should handle array notation by writing values to an array at the specified index', function() {
166 | object.collection = ['old'];
167 |
168 | nestedObjectHelper.writeAttribute(object, 'collection[0]', 'new');
169 |
170 | expect(object.collection).toBeTruthy();
171 | expect(object.collection[0]).toMatch('new');
172 | });
173 |
174 | it('should handle array notation with nested objects for indexes that already exist', function() {
175 | object.collection = [{
176 | foo: 'FOO',
177 | bar: 'BAR'
178 | }];
179 |
180 | nestedObjectHelper.writeAttribute(object, 'collection[0].bar', 'RAB');
181 | nestedObjectHelper.writeAttribute(object, 'collection[0].baz', 'BAZ');
182 |
183 | expect(object.collection).toBeTruthy();
184 | expect(object.collection[0].foo).toMatch('FOO');
185 | expect(object.collection[0].bar).toMatch('RAB');
186 | expect(object.collection[0].baz).toMatch('BAZ');
187 | });
188 | });
189 | });
190 |
--------------------------------------------------------------------------------
/tests/unit/services/parse.js:
--------------------------------------------------------------------------------
1 | describe('$parse', function() {
2 | 'use strict';
3 |
4 | beforeEach(module('formFor'));
5 |
6 | var $parse;
7 |
8 | beforeEach(inject(function ($injector) {
9 | $parse = $injector.get('$parse');
10 | }));
11 |
12 | describe('StringUtil', function() {
13 | it('should gracefully handle null and empty strings', function() {
14 | var data = {};
15 |
16 | $parse('names[0]').assign(data, 'value');
17 |
18 | expect(data.names[0]).toEqual('value');
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/tests/unit/services/promise-utils.js:
--------------------------------------------------------------------------------
1 | describe('PromiseUtils', function() {
2 | 'use strict';
3 |
4 | beforeEach(module('formFor'));
5 |
6 | var $q;
7 | var $rootScope;
8 | var promiseUtils;
9 |
10 | beforeEach(inject(function ($injector) {
11 | $q = $injector.get('$q');
12 | $rootScope = $injector.get('$rootScope');
13 | promiseUtils = new formFor.PromiseUtils($q);
14 | }));
15 |
16 | afterEach(inject(function() {
17 | $rootScope.$apply(); // Necessary for promise assertions below
18 | }));
19 |
20 | describe('resolve', function() {
21 | it('should function like reject', function() {
22 | expect(promiseUtils.resolve(true)).toBeResolvedWith(true);
23 | });
24 | });
25 |
26 | describe('waitForAll', function() {
27 | var deferred1, deferred2, deferred3;
28 | var waitForAll, resolution, rejection;
29 |
30 | beforeEach(function() {
31 | deferred1 = $q.defer();
32 | deferred2 = $q.defer();
33 | deferred3 = $q.defer();
34 |
35 | resolution = rejection = null;
36 |
37 | waitForAll = promiseUtils.waitForAll([deferred1.promise, deferred2.promise, deferred3.promise]);
38 | waitForAll.then(
39 | function(data) {
40 | resolution = data;
41 | },
42 | function(data) {
43 | rejection = data;
44 | });
45 | });
46 |
47 | it('should resolve immediately if sent an empty collecton of promises', function() {
48 | waitForAll = promiseUtils.waitForAll([]);
49 |
50 | expect(waitForAll).toBeResolved();
51 | });
52 |
53 | it('should wait until all inner promises have been resolved or rejected before completing', function() {
54 | $rootScope.$apply(); // Force any pending resolutions
55 |
56 | expect(rejection).toBeFalsy();
57 | expect(resolution).toBeFalsy();
58 |
59 | deferred1.resolve();
60 |
61 | $rootScope.$apply(); // Force any pending resolutions
62 |
63 | expect(rejection).toBeFalsy();
64 | expect(resolution).toBeFalsy();
65 |
66 | deferred2.resolve();
67 |
68 | $rootScope.$apply(); // Force any pending resolutions
69 |
70 | expect(rejection).toBeFalsy();
71 | expect(resolution).toBeFalsy();
72 |
73 | expect(waitForAll).toBeResolved();
74 |
75 | deferred3.resolve();
76 |
77 | $rootScope.$apply(); // Force any pending resolutions
78 |
79 | expect(resolution).toBeTruthy();
80 | });
81 |
82 | it('should resolve if inner promises resolve', function() {
83 | deferred1.resolve();
84 | deferred2.resolve();
85 | deferred3.resolve();
86 |
87 | expect(waitForAll).toBeResolved();
88 | });
89 |
90 | it('should reject if any inner promises reject', function() {
91 | deferred1.resolve();
92 | deferred2.reject();
93 | deferred3.resolve();
94 |
95 | expect(waitForAll).toBeRejected();
96 | });
97 |
98 | it('should return an array of data containing the datum from each resolved/rejected promise', function() {
99 | deferred1.resolve('resolution 1');
100 | deferred2.reject('rejection 2');
101 | deferred3.resolve('resolution 3');
102 |
103 | $rootScope.$apply(); // Force any pending resolutions
104 |
105 | expect(rejection[0]).toEqual('resolution 1');
106 | expect(rejection[1]).toEqual('rejection 2');
107 | expect(rejection[2]).toEqual('resolution 3');
108 | });
109 | });
110 |
111 | });
112 |
--------------------------------------------------------------------------------
/tests/unit/services/string-util.js:
--------------------------------------------------------------------------------
1 | describe('StringUtil', function() {
2 | 'use strict';
3 |
4 | beforeEach(module('formFor'));
5 |
6 | describe('StringUtil', function() {
7 | it('should gracefully handle null and empty strings', function() {
8 | expect(formFor.StringUtil.humanize(null)).toEqual('');
9 | expect(formFor.StringUtil.humanize(undefined)).toEqual('');
10 | expect(formFor.StringUtil.humanize('')).toEqual('');
11 | });
12 |
13 | it('should convert snake-case variables to humanized strings', function() {
14 | expect(formFor.StringUtil.humanize('snake_case')).toEqual('Snake Case');
15 | expect(formFor.StringUtil.humanize('snake_case_too')).toEqual('Snake Case Too');
16 | });
17 |
18 | it('should convert camel-case variables to humanized strings', function() {
19 | expect(formFor.StringUtil.humanize('camelCase')).toEqual('Camel Case');
20 | expect(formFor.StringUtil.humanize('camelCaseToo')).toEqual('Camel Case Too');
21 | });
22 |
23 | it('should not convert already-humanized strings', function() {
24 | expect(formFor.StringUtil.humanize('Word')).toEqual('Word');
25 | expect(formFor.StringUtil.humanize('Humanized String')).toEqual('Humanized String');
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------