diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 86afda1233..ab7be52f51 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -13,345 +13,346 @@ **/ function valPropertyMsg(serverValidationManager, localizationService, angularHelper) { - return { - require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'], - replace: true, - restrict: "E", - template: "
{{errorMsg}}
", - scope: {}, - link: function (scope, element, attrs, ctrl) { + return { + require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'], + replace: true, + restrict: "E", + template: "
", + scope: {}, + link: function (scope, element, attrs, ctrl) { - var unsubscribe = []; - var watcher = null; - var hasError = false; // tracks if there is a child error or an explicit error + var unsubscribe = []; + var watcher = null; + var hasError = false; // tracks if there is a child error or an explicit error - //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; + //create properties on our custom scope so we can use it in our template + scope.errorMsg = ""; - //the property form controller api - var formCtrl = ctrl[0]; - //the valFormManager controller api - var valFormManager = ctrl[1]; - //the property controller api - var umbPropCtrl = ctrl[2]; - //the variants controller api - var umbVariantCtrl = ctrl[3]; + //the property form controller api + var formCtrl = ctrl[0]; + //the valFormManager controller api + var valFormManager = ctrl[1]; + //the property controller api + var umbPropCtrl = ctrl[2]; + //the variants controller api + var umbVariantCtrl = ctrl[3]; - var currentProperty = umbPropCtrl.property; - scope.currentProperty = currentProperty; + var currentProperty = umbPropCtrl.property; + scope.currentProperty = currentProperty; - var currentCulture = currentProperty.culture; - var currentSegment = currentProperty.segment; + var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; - // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) - var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) + var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; - var labels = {}; - var showValidation = false; + var labels = {}; + var showValidation = false; - if (umbVariantCtrl) { - //if we are inside of an umbVariantContent directive + if (umbVariantCtrl) { + //if we are inside of an umbVariantContent directive - var currentVariant = umbVariantCtrl.editor.content; + var currentVariant = umbVariantCtrl.editor.content; - // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { - //This property is locked cause its a invariant property shown on a non-default language. - //Therefor do not validate this field. - return; - } - } + // Lets check if we have variants and we are on the default language then ... + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { + //This property is locked cause its a invariant property shown on a non-default language. + //Therefor do not validate this field. + return; + } + } - // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. - currentCulture = currentCulture || "invariant"; + // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. + currentCulture = currentCulture || "invariant"; - // Gets the error message to display - function getErrorMsg() { - //this can be null if no property was assigned - if (scope.currentProperty) { - //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, "", null); - //if there's an error message use it - if (err && err.errorMsg) { - return err.errorMsg; - } - else { - return scope.currentProperty.propertyErrorMessage ? scope.currentProperty.propertyErrorMessage : labels.propertyHasErrors; - } + // Gets the error message to display + function getErrorMsg() { + //this can be null if no property was assigned + if (scope.currentProperty) { + //first try to get the error msg from the server collection + var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, "", null); + //if there's an error message use it + if (err && err.errorMsg) { - } - return labels.propertyHasErrors; - } + return err.errorMsg; + } + else { + return scope.currentProperty.propertyErrorMessage ? scope.currentProperty.propertyErrorMessage : labels.propertyHasErrors; + } - // check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors - // we can check if those are valPropertyMsg or valServer and can clear our error in those cases. - function checkAndClearError() { + } + return labels.propertyHasErrors; + } - var errCount = angularHelper.countAllFormErrors(formCtrl); + // check the current errors in the form (and recursive sub forms), if there is 1 or 2 errors + // we can check if those are valPropertyMsg or valServer and can clear our error in those cases. + function checkAndClearError() { - if (errCount === 0) { - return true; - } + var errCount = angularHelper.countAllFormErrors(formCtrl); - if (errCount > 2) { - return false; - } - - var hasValServer = Utilities.isArray(formCtrl.$error.valServer); - if (errCount === 1 && hasValServer) { - return true; - } - - var hasOwnErr = hasExplicitError(); - if ((errCount === 1 && hasOwnErr) || (errCount === 2 && hasOwnErr && hasValServer)) { - - var propertyValidationPath = umbPropCtrl.getValidationPath(); - // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves - if (isLastServerError(propertyValidationPath)) { - serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); - return true; - } - return false; - } - - return false; - } - - // returns true if there is an explicit valPropertyMsg validation error on the form - function hasExplicitError() { - return Utilities.isArray(formCtrl.$error.valPropertyMsg); - } - - // returns true if there is only a single server validation error for this property validation key in it's validation path - function isLastServerError(propertyValidationPath) { - var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( - propertyValidationPath, - currentCulture, - currentSegment, - { matchType: "prefix" }); - if (nestedErrs.length === 0 || (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath)) { - - return true; - } - return false; - } - - // a custom $validator function called on when each child ngModelController changes a value. - function resetServerValidityValidator(fieldController) { - const storedFieldController = fieldController; // pin a reference to this - return (modelValue, viewValue) => { - // if the ngModelController value has changed, then we can check and clear the error - if (storedFieldController.$dirty) { - if (checkAndClearError()) { - resetError(); - } - } - return true; // this validator is always 'valid' - }; - } - - // collect all ng-model controllers recursively within the umbProperty form - // until it reaches the next nested umbProperty form - function collectAllNgModelControllersRecursively(controls, ngModels) { - controls.forEach(ctrl => { - if (angularHelper.isForm(ctrl)) { - // if it's not another umbProperty form then recurse - if (ctrl.$name !== formCtrl.$name) { - collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels); - } - } - else if (Object.prototype.hasOwnProperty.call(ctrl, '$modelValue') && Utilities.isObject(ctrl.$validators)) { - ngModels.push(ctrl); - } - }); - } - - // We start the watch when there's server validation errors detected. - // We watch on the current form's properties and on first watch or if they are dynamically changed - // we find all ngModel controls recursively on this form (but stop recursing before we get to the next) - // umbProperty form). Then for each ngModelController we assign a new $validator. This $validator - // will execute whenever the value is changed which allows us to check and reset the server validator - function startWatch() { - if (!watcher) { - - watcher = scope.$watchCollection( - () => formCtrl, - function (updatedFormController) { - let childControls = updatedFormController.$getControls(); - let ngModels = []; - collectAllNgModelControllersRecursively(childControls, ngModels); - ngModels.forEach(x => { - if (!x.$validators.serverValidityResetter) { - x.$validators.serverValidityResetter = resetServerValidityValidator(x); - } - }); - }); - } - } - - //clear the watch when the property validator is valid again - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } - } - - function resetError() { - stopWatch(); - hasError = false; - formCtrl.$setValidity('valPropertyMsg', true); - scope.errorMsg = ""; - - } - - // This deals with client side validation changes and is executed anytime validators change on the containing - // valFormManager. This allows us to know when to display or clear the property error data for non-server side errors. - function checkValidationStatus() { - if (formCtrl.$invalid) { - //first we need to check if the valPropertyMsg validity is invalid - if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { - //since we already have an error we'll just return since this means we've already set the - //hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe - return; - } - //if there are any errors in the current property form that are not valPropertyMsg - else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { - - // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared - if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { - - resetError(); - - // if there's no value, the controls can be reset, which clears the error state on formCtrl - for (let control of formCtrl.$getControls()) { - control.$setValidity(); - } - - return; - } - - hasError = true; - //update the validation message if we don't already have one assigned. - if (showValidation && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - } - } - else { - resetError(); - } - } - else { - resetError(); - } - } - - function onInit() { - - localizationService.localize("errors_propertyHasErrors").then(function (data) { - - labels.propertyHasErrors = data; - - //if there's any remaining errors in the server validation service then we should show them. - showValidation = serverValidationManager.items.length > 0; - if (!showValidation) { - //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) - //or we can just check upwards in the DOM for the css class (easier for now). - //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot - //reset the status. - showValidation = element.closest(".show-validation").length > 0; - } - - //listen for form validation changes. - //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then - // subscribing to this single watch. - // TODO: Really? Since valFormManager is watching a countof all errors which is more overhead than watching formCtrl.$invalid - // and there's a TODO there that it should just watch formCtrl.$invalid - valFormManager.onValidationStatusChanged(function () { - checkValidationStatus(); - }); - - //listen for the forms saving event - unsubscribe.push(scope.$on("formSubmitting", function () { - showValidation = true; - if (hasError && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - startWatch(); - } - else if (!hasError) { - resetError(); - } - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on("formSubmitted", function () { - showValidation = false; - resetError(); - })); - - if (scope.currentProperty) { //this can be null if no property was assigned - - // listen for server validation changes for property validation path prefix. - // We pass in "" in order to listen for all validation changes to the content property, not for - // validation changes to fields in the property this is because some server side validators may not - // return the field name for which the error belongs too, just the property for which it belongs. - // It's important to note that we need to subscribe to server validation changes here because we always must - // indicate that a content property is invalid at the property level since developers may not actually implement - // the correct field validation in their property editors. - - const serverValidationManagerCallback = function(isValid, propertyErrors) { - var hadError = hasError; - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); - startWatch(); - - // This check is required in order to be able to reset ourselves and is typically for complex editor - // scenarios where the umb-property itself doesn't contain any ng-model controls which means that the - // above serverValidityResetter technique will not work to clear valPropertyMsg errors. - // In order for this to work we rely on the current form controller's $pristine state. This means that anytime - // the form is submitted whether there are validation errors or not the state must be reset... this is automatically - // taken care of with the formHelper.resetForm method that should be used in all cases. $pristine is required because it's - // a value that is cascaded to all form controls based on the hierarchy of child ng-model controls. This allows us to easily - // know if a value has changed. The alternative is what we used to do which was to put a deep $watch on the entire complex value - // which is hugely inefficient. - if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) { - var propertyValidationPath = umbPropCtrl.getValidationPath(); - serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); - resetError(); - } - } - else { - resetError(); - } - } - - unsubscribe.push(serverValidationManager.subscribe( - umbPropCtrl.getValidationPath(), - currentCulture, - "", - serverValidationManagerCallback, - currentSegment, - { matchType: "prefix" } // match property validation path prefix - )); - } - - }); - } - - //when the scope is disposed we need to unsubscribe - scope.$on('$destroy', function () { - stopWatch(); - unsubscribe.forEach(u => u()); - }); - - onInit(); + if (errCount === 0) { + return true; } + if (errCount > 2) { + return false; + } - }; + var hasValServer = Utilities.isArray(formCtrl.$error.valServer); + if (errCount === 1 && hasValServer) { + return true; + } + + var hasOwnErr = hasExplicitError(); + if ((errCount === 1 && hasOwnErr) || (errCount === 2 && hasOwnErr && hasValServer)) { + + var propertyValidationPath = umbPropCtrl.getValidationPath(); + // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves + if (isLastServerError(propertyValidationPath)) { + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); + return true; + } + return false; + } + + return false; + } + + // returns true if there is an explicit valPropertyMsg validation error on the form + function hasExplicitError() { + return Utilities.isArray(formCtrl.$error.valPropertyMsg); + } + + // returns true if there is only a single server validation error for this property validation key in it's validation path + function isLastServerError(propertyValidationPath) { + var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( + propertyValidationPath, + currentCulture, + currentSegment, + { matchType: "prefix" }); + if (nestedErrs.length === 0 || (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath)) { + + return true; + } + return false; + } + + // a custom $validator function called on when each child ngModelController changes a value. + function resetServerValidityValidator(fieldController) { + const storedFieldController = fieldController; // pin a reference to this + return (modelValue, viewValue) => { + // if the ngModelController value has changed, then we can check and clear the error + if (storedFieldController.$dirty) { + if (checkAndClearError()) { + resetError(); + } + } + return true; // this validator is always 'valid' + }; + } + + // collect all ng-model controllers recursively within the umbProperty form + // until it reaches the next nested umbProperty form + function collectAllNgModelControllersRecursively(controls, ngModels) { + controls.forEach(ctrl => { + if (angularHelper.isForm(ctrl)) { + // if it's not another umbProperty form then recurse + if (ctrl.$name !== formCtrl.$name) { + collectAllNgModelControllersRecursively(ctrl.$getControls(), ngModels); + } + } + else if (Object.prototype.hasOwnProperty.call(ctrl, '$modelValue') && Utilities.isObject(ctrl.$validators)) { + ngModels.push(ctrl); + } + }); + } + + // We start the watch when there's server validation errors detected. + // We watch on the current form's properties and on first watch or if they are dynamically changed + // we find all ngModel controls recursively on this form (but stop recursing before we get to the next) + // umbProperty form). Then for each ngModelController we assign a new $validator. This $validator + // will execute whenever the value is changed which allows us to check and reset the server validator + function startWatch() { + if (!watcher) { + + watcher = scope.$watchCollection( + () => formCtrl, + function (updatedFormController) { + let childControls = updatedFormController.$getControls(); + let ngModels = []; + collectAllNgModelControllersRecursively(childControls, ngModels); + ngModels.forEach(x => { + if (!x.$validators.serverValidityResetter) { + x.$validators.serverValidityResetter = resetServerValidityValidator(x); + } + }); + }); + } + } + + //clear the watch when the property validator is valid again + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + function resetError() { + stopWatch(); + hasError = false; + formCtrl.$setValidity('valPropertyMsg', true); + scope.errorMsg = ""; + + } + + // This deals with client side validation changes and is executed anytime validators change on the containing + // valFormManager. This allows us to know when to display or clear the property error data for non-server side errors. + function checkValidationStatus() { + if (formCtrl.$invalid) { + //first we need to check if the valPropertyMsg validity is invalid + if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { + //since we already have an error we'll just return since this means we've already set the + //hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe + return; + } + //if there are any errors in the current property form that are not valPropertyMsg + else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { + + // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared + if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { + + resetError(); + + // if there's no value, the controls can be reset, which clears the error state on formCtrl + for (let control of formCtrl.$getControls()) { + control.$setValidity(); + } + + return; + } + + hasError = true; + //update the validation message if we don't already have one assigned. + if (showValidation && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + } + } + else { + resetError(); + } + } + else { + resetError(); + } + } + + function onInit() { + + localizationService.localize("errors_propertyHasErrors").then(function (data) { + + labels.propertyHasErrors = data; + + //if there's any remaining errors in the server validation service then we should show them. + showValidation = serverValidationManager.items.length > 0; + if (!showValidation) { + //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) + //or we can just check upwards in the DOM for the css class (easier for now). + //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot + //reset the status. + showValidation = element.closest(".show-validation").length > 0; + } + + //listen for form validation changes. + //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then + // subscribing to this single watch. + // TODO: Really? Since valFormManager is watching a countof all errors which is more overhead than watching formCtrl.$invalid + // and there's a TODO there that it should just watch formCtrl.$invalid + valFormManager.onValidationStatusChanged(function () { + checkValidationStatus(); + }); + + //listen for the forms saving event + unsubscribe.push(scope.$on("formSubmitting", function () { + showValidation = true; + if (hasError && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + startWatch(); + } + else if (!hasError) { + resetError(); + } + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on("formSubmitted", function () { + showValidation = false; + resetError(); + })); + + if (scope.currentProperty) { //this can be null if no property was assigned + + // listen for server validation changes for property validation path prefix. + // We pass in "" in order to listen for all validation changes to the content property, not for + // validation changes to fields in the property this is because some server side validators may not + // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. + + const serverValidationManagerCallback = function (isValid, propertyErrors) { + var hadError = hasError; + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + + // This check is required in order to be able to reset ourselves and is typically for complex editor + // scenarios where the umb-property itself doesn't contain any ng-model controls which means that the + // above serverValidityResetter technique will not work to clear valPropertyMsg errors. + // In order for this to work we rely on the current form controller's $pristine state. This means that anytime + // the form is submitted whether there are validation errors or not the state must be reset... this is automatically + // taken care of with the formHelper.resetForm method that should be used in all cases. $pristine is required because it's + // a value that is cascaded to all form controls based on the hierarchy of child ng-model controls. This allows us to easily + // know if a value has changed. The alternative is what we used to do which was to put a deep $watch on the entire complex value + // which is hugely inefficient. + if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) { + var propertyValidationPath = umbPropCtrl.getValidationPath(); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); + resetError(); + } + } + else { + resetError(); + } + } + + unsubscribe.push(serverValidationManager.subscribe( + umbPropCtrl.getValidationPath(), + currentCulture, + "", + serverValidationManagerCallback, + currentSegment, + { matchType: "prefix" } // match property validation path prefix + )); + } + + }); + } + + //when the scope is disposed we need to unsubscribe + scope.$on('$destroy', function () { + stopWatch(); + unsubscribe.forEach(u => u()); + }); + + onInit(); + } + + + }; } angular.module('umbraco.directives.validation').directive("valPropertyMsg", valPropertyMsg);