diff --git a/src/angular-activerecord.js b/src/angular-activerecord.js index 85585ac..bd67811 100644 --- a/src/angular-activerecord.js +++ b/src/angular-activerecord.js @@ -93,7 +93,7 @@ angular.module('ActiveRecord', []).factory('ActiveRecord', ['$http', '$q', '$par /** * Determine if the model has changed since the last sync (fetch/load). - * + * * @param {String} [property] Determine if that specific property has changed. * @returns {Boolean} */ @@ -127,7 +127,7 @@ angular.module('ActiveRecord', []).factory('ActiveRecord', ['$http', '$q', '$par } } for (var property in current) { - if (current.hasOwnProperty(property)) { + if (current.hasOwnProperty(property) && property.indexOf("$") !== 0) { var value = current[property]; if (typeof value !== 'function' && angular.equals(value, previousAttributes[property]) === false) { changed[property] = value; @@ -181,6 +181,80 @@ angular.module('ActiveRecord', []).factory('ActiveRecord', ['$http', '$q', '$par return deferred.promise; }, + $validationErrorMessages: {}, + + $validations: {}, + + $fieldTranslations: {}, + + $errors: {}, + + $isValid: function(fieldName) { + var valid = false; + if (Object.keys(this.$errors).length === 0) { + valid = true; + } else if (fieldName && !this.$errors[fieldName]) { + valid = true; + } + return valid; + }, + + $validateOne: function(fieldName) { + var errors = []; + delete this.$errors[fieldName]; + if (this.$validations[fieldName]) { + var mthis = this; + if (mthis[fieldName]) { + angular.forEach(this.$validations[fieldName], function(validationValue, functionName) { + var $functionName = "$" + functionName; + if (functionName != "required" && mthis[$functionName]) { + var value = validationValue; + var errorMessage = null; + if (angular.isObject(validationValue)) { + if (validationValue.value) value = validationValue.value; + if (validationValue.message) errorMessage = validationValue.message; + } + var res = mthis[$functionName](mthis[fieldName], value); + if (res !== true) { + if (!errorMessage) errorMessage = mthis.$validationErrorMessages[functionName] || "is invalid"; + if (angular.isFunction(errorMessage)) errorMessage = errorMessage(fieldName, mthis[fieldName], value); + if (typeof sprintf !== "undefined") { + errorMessage = sprintf(errorMessage, {fieldName: mthis.$fieldTranslations[fieldName] || fieldName, fieldValue: mthis[fieldName], validationValue: value}); + } + errors.push(errorMessage); + } + } + }); + } else if (this.$validations[fieldName].required) { + var errMessage = null; + if (angular.isObject(this.$validations[fieldName].required) && this.$validations[fieldName].required.message) { + errMessage = this.$validations[fieldName].required.message; + } else if (this.$validationErrorMessages.required) { + errMessage = this.$validationErrorMessages.required; + } else { + errMessage = "is required"; + } + if (angular.isFunction(errMessage)) errMessage = errMessage(fieldName); + errors.push(errMessage); + } + } + if (errors.length) { + this.$errors[fieldName] = errors; + } + return this.$isValid(fieldName); + }, + + $validate: function(fieldName) { + if (fieldName) return this.$validateOne(fieldName); + + var mthis = this; + this.$errors = {}; + angular.forEach(this.$validations, function(validation, validationKey) { + mthis.$validateOne(validationKey); + }); + return this.$isValid(); + }, + /** * Save the record to the backend. * @param {Object} [values] Set these values before saving the record. @@ -198,6 +272,11 @@ angular.module('ActiveRecord', []).factory('ActiveRecord', ['$http', '$q', '$par } var operation = this.$isNew() ? 'create' : 'update'; var model = this; + if (!model.$validate()) { + var deferred = $q.defer(); + deferred.reject(model.$errors); + return deferred.promise; + } options = options || {}; var filters = _result(this, '$writeFilters'); if (filters) { diff --git a/test/ActiveRecordSpec.js b/test/ActiveRecordSpec.js index 8f89058..c0114fd 100644 --- a/test/ActiveRecordSpec.js +++ b/test/ActiveRecordSpec.js @@ -127,6 +127,99 @@ describe("ActiveRecord", function() { $httpBackend.flush(); }); + it("save with validation success", function() { + $httpBackend.expectPOST('/resources', '{"number":8,"title":"Henry V"}').respond('{"number":8,"title":"Henry V"}'); + var Model = ActiveRecord.extend({ + $urlRoot: '/resources', + + $min: function(fieldValue, validationValue) { + fieldValue = parseInt(fieldValue, 10); + validationValue = parseInt(validationValue, 10); + return fieldValue >= validationValue; + }, + + $validations: { + number: {min: 5}, + title: {required: true}, + } + }); + var model = new Model(); + model.number = 8; + model.title = "Henry V"; + model.$save().then(function(result) { + expect(result).toBe(model); + expect(model.number).toBe(8); + expect(model.title).toBe('Henry V'); + expect(model.$isValid()).toBe(true); + }); + $httpBackend.flush(); + }); + + it("save with validation failed", function() { + var Model = ActiveRecord.extend({ + $urlRoot: '/resources', + + $min: function(fieldValue, validationValue) { + fieldValue = parseInt(fieldValue, 10); + validationValue = parseInt(validationValue, 10); + return fieldValue >= validationValue; + }, + + $validations: { + number: {min: 5}, + name: {required: true}, + anOtherNumber: {min: 5} + } + }); + var model = new Model(); + model.number = 3; + model.$save().catch(function(err) { + expect(Object.keys(err).length).toBe(2); + expect(err.number[0]).toBe("is invalid"); + expect(err.name[0]).toBe("is required"); + expect(model.$isValid()).toBe(false); + }); + }); + + it("save with validation failed and custom messages", function() { + var Model = ActiveRecord.extend({ + $urlRoot: '/resources', + + $min: function(fieldValue, validationValue) { + fieldValue = parseInt(fieldValue, 10); + validationValue = parseInt(validationValue, 10); + return fieldValue >= validationValue; + }, + + $validationErrorMessages: { + min: "invalid1", + required: "required1" + }, + + $validations: { + invalidField1: {min: 5}, + invalidField2: {min: {value: 5, message: "invalid2"}}, + invalidField3: {min: {value: 5, message: function() { return "invalid3"}}}, + requiredField1: {required: true}, + requiredField2: {required: {message: "required2"}}, + requiredField3: {required: {message: function() {return "required3"}}} + } + }); + var model = new Model(); + model.invalidField1 = 3; + model.invalidField2 = 3; + model.invalidField3 = 3; + model.$save().catch(function(err) { + expect(err.invalidField1[0]).toBe("invalid1"); + expect(err.invalidField2[0]).toBe("invalid2"); + expect(err.invalidField3[0]).toBe("invalid3"); + expect(err.requiredField1[0]).toBe("required1"); + expect(err.requiredField2[0]).toBe("required2"); + expect(err.requiredField3[0]).toBe("required3"); + expect(model.$isValid()).toBe(false); + }); + }); + it("delete", function() { $httpBackend.expectDELETE('/resources/1').respond(''); var model = createBasicModel();