Lots of work for validation. We have a different type of validation model in Umbraco where we still save content even if some things are invalid (we just don't publish) so we have to take all of this into account. We also have other rules where if it is new content but required fields like 'name' are empty we cannot continue to save. Also started working on dealing with server side validation errors for content fields (not just user defined fields).

This commit is contained in:
Shannon
2013-07-16 18:23:20 +10:00
parent c7b4cfd375
commit e2fa610358
20 changed files with 564 additions and 281 deletions

View File

@@ -53,7 +53,7 @@ function valPropertyMsg(serverValidationService) {
hasError = true;
//update the validation message if we don't already have one assigned.
if (showValidation && scope.errorMsg === "") {
scope.errorMsg = serverValidationService.getError(scope.currentProperty, "");
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
}
}
else {
@@ -71,7 +71,7 @@ function valPropertyMsg(serverValidationService) {
scope.$on("saving", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = serverValidationService.getError(scope.currentProperty, "");
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
}
else if (!hasError) {
scope.errorMsg = "";
@@ -111,7 +111,7 @@ function valPropertyMsg(serverValidationService) {
//error collection... it is a 'one-time' usage so that when the field is invalidated
//again, the message we display is the client side message.
//NOTE: 'this' in the subscribe callback context is the validation manager object.
this.removeError(scope.currentProperty);
this.removePropertyError(scope.currentProperty);
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
}

View File

@@ -4,7 +4,9 @@
* @function
*
* @description
* used to handle server side validation and wires up the UI with the messages
* Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one
* is for user defined properties (called Properties) and the other is for field properties which are attached to the native
* model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
*/
function serverValidationService() {
@@ -23,35 +25,60 @@ function serverValidationService() {
* a particular field, otherwise we can only pinpoint that there is an error for a content property, not the
* property's specific field. This is used with the val-server directive in which the directive specifies the
* field alias to listen for.
* If contentProperty is null, then this subscription is for a field property (not a user defined property)
*/
subscribe: function (contentProperty, fieldName, callback) {
if (!contentProperty || !callback) {
if (!callback) {
return;
}
//don't add it if it already exists
var exists = _.find(callbacks, function(item) {
return item.propertyAlias === contentProperty.alias && item.fieldName === fieldName;
});
if (!exists) {
callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
}
if (contentProperty === null) {
//don't add it if it already exists
var exists1 = _.find(callbacks, function (item) {
return item.propertyAlias === null && item.fieldName === fieldName;
});
if (!exists1) {
callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
}
}
else if (contentProperty !== undefined) {
//don't add it if it already exists
var exists2 = _.find(callbacks, function (item) {
return item.propertyAlias === contentProperty.alias && item.fieldName === fieldName;
});
if (!exists2) {
callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
}
}
},
unsubscribe: function(contentProperty, fieldName) {
if (!contentProperty) {
return;
if (contentProperty === null) {
//remove all callbacks for the content field
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === null && item.fieldName === fieldName;
});
}
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === contentProperty.alias &&
(item.fieldName === fieldName ||
((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
});
else if (contentProperty !== undefined) {
//remove all callbacks for the content property
callbacks = _.reject(callbacks, function (item) {
return item.propertyAlias === contentProperty.alias &&
(item.fieldName === fieldName ||
((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
});
}
},
/**
* @ngdoc function
* @name getCallbacks
* @name getPropertyCallbacks
* @methodOf umbraco.services.serverValidationService
* @function
*
@@ -60,7 +87,7 @@ function serverValidationService() {
* This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an
* explicit field name set.
*/
getCallbacks: function (contentProperty, fieldName) {
getPropertyCallbacks: function (contentProperty, fieldName) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field and for only the property
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === "")));
@@ -70,20 +97,75 @@ function serverValidationService() {
/**
* @ngdoc function
* @name addError
* @name getFieldCallbacks
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Gets all callbacks that has been registered using the subscribe method for the field.
*/
getFieldCallbacks: function (contentProperty, fieldName) {
var found = _.filter(callbacks, function (item) {
//returns any callback that have been registered directly against the field
return (item.propertyAlias === null && item.fieldName === fieldName);
});
return found;
},
/**
* @ngdoc function
* @name addFieldError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Adds an error message for a native content item field (not a user defined property, for Example, 'Name')
*/
addFieldError: function(fieldName, errorMsg) {
if (!fieldName) {
return;
}
//only add the item if it doesn't exist
if (!this.hasFieldError(fieldName)) {
this.items.push({
propertyAlias: null,
fieldName: fieldName,
errorMsg: errorMsg
});
}
//find all errors for this item
var errorsForCallback = _.filter(this.items, function (item) {
return (item.propertyAlias === null && item.fieldName === fieldName);
});
//we should now call all of the call backs registered for this error
var cbs = this.getFieldCallbacks(fieldName);
//call each callback for this error
for (var cb in cbs) {
cbs[cb].callback.apply(this, [
false, //pass in a value indicating it is invalid
errorsForCallback, //pass in the errors for this item
this.items]); //pass in all errors in total
}
},
/**
* @ngdoc function
* @name addPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Adds an error message for the content property
*/
addError: function (contentProperty, fieldName, errorMsg) {
addPropertyError: function (contentProperty, fieldName, errorMsg) {
if (!contentProperty) {
return;
}
//only add the item if it doesn't exist
if (!this.hasError(contentProperty, fieldName)) {
if (!this.hasPropertyError(contentProperty, fieldName)) {
this.items.push({
propertyAlias: contentProperty.alias,
fieldName: fieldName,
@@ -96,7 +178,7 @@ function serverValidationService() {
return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName);
});
//we should now call all of the call backs registered for this error
var cbs = this.getCallbacks(contentProperty, fieldName);
var cbs = this.getPropertyCallbacks(contentProperty, fieldName);
//call each callback for this error
for (var cb in cbs) {
cbs[cb].callback.apply(this, [
@@ -108,14 +190,14 @@ function serverValidationService() {
/**
* @ngdoc function
* @name removeError
* @name removePropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Removes an error message for the content property
*/
removeError: function (contentProperty, fieldName) {
removePropertyError: function (contentProperty, fieldName) {
if (!contentProperty) {
return;
@@ -147,14 +229,14 @@ function serverValidationService() {
/**
* @ngdoc function
* @name getError
* @name getPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Gets the error message for the content property
*/
getError: function (contentProperty, fieldName) {
getPropertyError: function (contentProperty, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
@@ -165,14 +247,32 @@ function serverValidationService() {
/**
* @ngdoc function
* @name hasError
* @name getFieldError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Gets the error message for a content field
*/
getFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.fieldName === fieldName);
});
//return generic property error message if the error doesn't exist
return err ? err : "Field has errors";
},
/**
* @ngdoc function
* @name hasPropertyError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Checks if the content property + field name combo has an error
*/
hasError: function (contentProperty, fieldName) {
hasPropertyError: function (contentProperty, fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
@@ -180,6 +280,23 @@ function serverValidationService() {
return err ? true : false;
},
/**
* @ngdoc function
* @name hasFieldError
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Checks if a content field has an error
*/
hasFieldError: function (fieldName) {
var err = _.find(this.items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === null && item.fieldName === fieldName);
});
return err ? true : false;
},
/** The array of error messages */
items: []
};

View File

@@ -8,6 +8,61 @@
*/
function ContentEditController($scope, $routeParams, contentResource, notificationsService, angularHelper, serverValidationService) {
/**
* @ngdoc function
* @name handleValidationErrors
* @methodOf ContentEditController
* @function
*
* @description
* A function to handle the validation (ModelState) errors collection which will happen on a 403 error indicating validation errors
*/
function handleValidationErrors(modelState) {
//get a list of properties since they are contained in tabs
var allProps = [];
for (var i = 0; i < $scope.content.tabs.length; i++) {
for (var p = 0; p < $scope.content.tabs[i].properties.length; p++) {
allProps.push($scope.content.tabs[i].properties[p]);
}
}
for (var e in modelState) {
//the alias in model state can be in dot notation which indicates
// * the first part is the content property alias
// * the second part is the field to which the valiation msg is associated with
//There will always be at least 2 parts since all model errors for properties are prefixed with "Properties"
var parts = e.split(".");
if (parts.length > 1) {
var propertyAlias = parts[1];
//find the content property for the current error
var contentProperty = _.find(allProps, function(item) {
return (item.alias === propertyAlias);
});
if (contentProperty) {
//if it contains 2 '.' then we will wire it up to a property's field
if (parts.length > 2) {
//add an error with a reference to the field for which the validation belongs too
serverValidationService.addPropertyError(contentProperty, parts[2], modelState[e][0]);
}
else {
//add a generic error for the property, no reference to a specific field
serverValidationService.addPropertyError(contentProperty, "", modelState[e][0]);
}
}
}
else {
//the parts are only 1, this means its not a property but a native content property
serverValidationService.addFieldError(parts[0], modelState[e][0]);
}
//add to notifications
notificationsService.error("Validation", modelState[e][0]);
}
}
/**
* @ngdoc function
* @name handleSaveError
@@ -17,55 +72,19 @@ function ContentEditController($scope, $routeParams, contentResource, notificati
* @description
* A function to handle what happens when we have validation issues from the server side
*/
function handleSaveError(err) {
//When the status is a 403 status, we have validation errors.
//Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
//Or, some strange server error
if (err.status == 403) {
//now we need to look through all the validation errors
if (err.data && err.data.ModelState) {
//get a list of properties since they are contained in tabs
var allProps = [];
for (var i = 0; i < $scope.content.tabs.length; i++) {
for (var p = 0; p < $scope.content.tabs[i].properties.length; p++) {
allProps.push($scope.content.tabs[i].properties[p]);
}
}
for (var e in err.data.ModelState) {
//the alias in model state can be in dot notation which indicates
// * the first part is the content property alias
// * the second part is the field to which the valiation msg is associated with
var parts = e.split(".");
var propertyAlias = parts[0];
//find the content property for the current error
var contentProperty = _.find(allProps, function (item) {
return (item.alias === propertyAlias);
});
if (contentProperty) {
//if it contains a '.' then we will wire it up to a property's field
if (parts.length > 1) {
//add an error with a reference to the field for which the validation belongs too
serverValidationService.addError(contentProperty, parts[1], err.data.ModelState[e][0]);
}
else {
//add a generic error for the property, no reference to a specific field
serverValidationService.addError(contentProperty, "", err.data.ModelState[e][0]);
}
//add to notifications
notificationsService.error("Validation", err.data.ModelState[e][0]);
}
}
if (err.data && (err.data.ModelState)) {
handleValidationErrors(err.data.ModelState);
}
}
else {
//TODO: Implement an overlay showing the full YSOD like we had in v5
//alert("failed!");
notificationsService.error("Validation failed", err);
}
}

View File

@@ -1,6 +1,6 @@
<ng-form name="contentNameForm">
<div class="span4 control-group" ng-class="{error: contentNameForm.name.$invalid}">
<input name="name" type="text" placeholder="{{placeholder}}" headline ng-model="model" required />
<input name="name" type="text" placeholder="{{placeholder}}" headline ng-model="model" />
<span class="help-inline" val-msg-for="name" val-toggle-msg="required">Required</span>
</div>
</ng-form>