diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js index cd84a3b2ca..d84bb8e24d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js @@ -13,8 +13,18 @@ angular.module("umbraco.directives") restrict: 'E', replace: true, templateUrl: 'views/directives/umb-property.html', - link: function (scope, element, attrs, ctrl) { + //Define a controller for this directive to expose APIs to other directives + controller: function ($scope, $timeout) { + + var self = this; + + //set the API properties/methods + + self.property = $scope.property; + self.setPropertyError = function(errorMsg) { + $scope.property.propertyErrorMessage = errorMsg; + }; } }; }); \ No newline at end of file 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 ea4b1b82c8..d37f4c5271 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 @@ -9,22 +9,86 @@ * and when an error is detected for this property we'll show the error message. * In order for this directive to work, the valStatusChanged directive must be placed on the containing form. **/ -function valPropertyMsg(serverValidationManager) { +function valPropertyMsg(serverValidationManager) { + return { scope: { - property: "=property" + property: "=" }, require: "^form", //require that this directive is contained within an ngForm replace: true, //replace the element with the template restrict: "E", //restrict to element - template: "
{{errorMsg}}
", - + template: "
{{errorMsg}}
", + /** Our directive requries a reference to a form controller which gets passed in to this parameter */ link: function (scope, element, attrs, formCtrl) { + var watcher = null; + + // Gets the error message to display + function getErrorMsg() { + //this can be null if no property was assigned + if (scope.property) { + //first try to get the error msg from the server collection + var err = serverValidationManager.getPropertyError(scope.property.alias, ""); + //if there's an error message use it + if (err && err.errorMsg) { + return err.errorMsg; + } + else { + return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; + } + + } + return "Property has errors"; + } + + // We need to subscribe to any changes to our model (based on user input) + // This is required because when we have a server error we actually invalidate + // the form which means it cannot be resubmitted. + // So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch("property.value", function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + var errCount = 0; + for (var e in formCtrl.$error) { + if (angular.isArray(formCtrl.$error[e])) { + errCount++; + } + } + + //we are explicitly checking for valServer errors here, since we shouldn't auto clear + // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg + // is the only one, then we'll clear. + + if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + scope.errorMsg = ""; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }, true); + } + } + + //clear the watch when the property validator is valid again + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + //if there's any remaining errors in the server validation service then we should show them. var showValidation = serverValidationManager.items.length > 0; var hasError = false; @@ -33,7 +97,7 @@ function valPropertyMsg(serverValidationManager) { scope.errorMsg = ""; //listen for form error changes - scope.$on("valStatusChanged", function(evt, args) { + scope.$on("valStatusChanged", function (evt, args) { if (args.form.$invalid) { //first we need to check if the valPropertyMsg validity is invalid @@ -47,12 +111,7 @@ function valPropertyMsg(serverValidationManager) { hasError = true; //update the validation message if we don't already have one assigned. if (showValidation && scope.errorMsg === "") { - var err; - //this can be null if no property was assigned - if (scope.property) { - err = serverValidationManager.getPropertyError(scope.property.alias, ""); - } - scope.errorMsg = err ? err.errorMsg : "Property has errors"; + scope.errorMsg = getErrorMsg(); } } else { @@ -70,15 +129,11 @@ function valPropertyMsg(serverValidationManager) { scope.$on("formSubmitting", function (ev, args) { showValidation = true; if (hasError && scope.errorMsg === "") { - var err; - //this can be null if no property was assigned - if (scope.property) { - err = serverValidationManager.getPropertyError(scope.property.alias, ""); - } - scope.errorMsg = err ? err.errorMsg : "Property has errors"; + scope.errorMsg = getErrorMsg(); } else if (!hasError) { scope.errorMsg = ""; + stopWatch(); } }); @@ -86,37 +141,10 @@ function valPropertyMsg(serverValidationManager) { scope.$on("formSubmitted", function (ev, args) { showValidation = false; scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); }); - //We need to subscribe to any changes to our model (based on user input) - // This is required because when we have a server error we actually invalidate - // the form which means it cannot be resubmitted. - // So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - scope.$watch("property.value", function(newValue) { - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if (!newValue) { - return; - } - - var errCount = 0; - for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { - errCount++; - } - } - - if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - } - }, true); - //listen for server validation changes // NOTE: 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 @@ -124,27 +152,30 @@ function valPropertyMsg(serverValidationManager) { // 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. - + if (scope.property) { //this can be null if no property was assigned - serverValidationManager.subscribe(scope.property.alias, "", function(isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { hasError = !isValid; if (hasError) { //set the error message to the server message scope.errorMsg = propertyErrors[0].errorMsg; //flag that the current validator is invalid formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); } else { scope.errorMsg = ""; //flag that the current validator is valid formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); } }); //when the element is disposed we need to unsubscribe! // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain // but they are a different callback instance than the above. - element.bind('$destroy', function() { + element.bind('$destroy', function () { + stopWatch(); serverValidationManager.unsubscribe(scope.property.alias, ""); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js new file mode 100644 index 0000000000..1d04fd9b34 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -0,0 +1,67 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyValidator +* @restrict A +* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation + on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of + with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor + might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a + callback method to perform the validation. The result of this method must return an object in the format of + {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' } + The error message returned will also be displayed for the property level validation message. + This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler + angular validation directives instead since this will watch the entire model. +**/ + +function valPropertyValidator(serverValidationManager) { + return { + scope: { + valPropertyValidator: "=" + }, + + // The element must have ng-model attribute and be inside an umbProperty directive + require: ['ngModel', '^umbProperty'], + + restrict: "A", + + link: function (scope, element, attrs, ctrls) { + + var modelCtrl = ctrls[0]; + var propCtrl = ctrls[1]; + + // Check whether the scope has a valPropertyValidator method + if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) { + throw new Error('val-property-validator directive must specify a function to call'); + } + + var initResult = scope.valPropertyValidator(); + + // Validation method + var validate = function (viewValue) { + // Calls the validition method + var result = scope.valPropertyValidator(); + if (!result.errorKey || result.isValid === undefined || !result.errorMsg) { + throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg"; + } + if (result.isValid === true) { + // Tell the controller that the value is valid + modelCtrl.$setValidity(result.errorKey, true); + propCtrl.setPropertyError(null); + } + else { + // Tell the controller that the value is invalid + modelCtrl.$setValidity(result.errorKey, false); + propCtrl.setPropertyError(result.errorMsg); + } + }; + + // Formatters are invoked when the model is modified in the code. + modelCtrl.$formatters.push(validate); + + // Parsers are called as soon as the value in the form input is modified + modelCtrl.$parsers.push(validate); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valPropertyValidator", valPropertyValidator); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index fbc91dcb11..9d944aabcc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -7,14 +7,49 @@ **/ function valServer(serverValidationManager) { return { - require: 'ngModel', + require: ['ngModel', '^umbProperty'], restrict: "A", - link: function (scope, element, attr, ctrl) { - - if (!scope.model || !scope.model.alias){ - throw "valServer can only be used in the scope of a content property object"; + link: function (scope, element, attr, ctrls) { + + var modelCtrl = ctrls[0]; + var umbPropCtrl = ctrls[1]; + + var watcher = null; + + //Need to watch the value model for it to change, previously we had subscribed to + //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // doesn't specifically have a 2 way ng binding. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + if (modelCtrl.$invalid) { + modelCtrl.$setValidity('valServer', true); + stopWatch(); + } + }, true); + } } - var currentProperty = scope.model; + + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + var currentProperty = umbPropCtrl.property; //default to 'value' if nothing is set var fieldName = "value"; @@ -25,33 +60,20 @@ function valServer(serverValidationManager) { fieldName = attr.valServer; } } - - //Need to watch the value model for it to change, previously we had subscribed to - //ctrl.$viewChangeListeners but this is not good enough if you have an editor that - // doesn't specifically have a 2 way ng binding. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - scope.$watch(function() { - return ctrl.$modelValue; - }, function (newValue) { - if (ctrl.$invalid) { - ctrl.$setValidity('valServer', true); - } - }); //subscribe to the server validation changes serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { if (!isValid) { - ctrl.$setValidity('valServer', false); + modelCtrl.$setValidity('valServer', false); //assign an error msg property to the current validator - ctrl.errorMsg = propertyErrors[0].errorMsg; + modelCtrl.errorMsg = propertyErrors[0].errorMsg; + startWatch(); } else { - ctrl.$setValidity('valServer', true); + modelCtrl.$setValidity('valServer', true); //reset the error message - ctrl.errorMsg = ""; + modelCtrl.errorMsg = ""; + stopWatch(); } }); @@ -59,6 +81,7 @@ function valServer(serverValidationManager) { // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain // but they are a different callback instance than the above. element.bind('$destroy', function () { + stopWatch(); serverValidationManager.unsubscribe(currentProperty.alias, fieldName); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index 89bc626d6a..f14492e88a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -2,11 +2,23 @@ function ColorPickerController($scope) { $scope.toggleItem = function (color) { if ($scope.model.value == color) { $scope.model.value = ""; + //this is required to re-validate + $scope.propertyForm.modelValue.$setViewValue($scope.model.value); } else { $scope.model.value = color; + //this is required to re-validate + $scope.propertyForm.modelValue.$setViewValue($scope.model.value); } }; + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) + $scope.validateMandatory = function () { + return { + isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value != ""), + errorMsg: "Value cannot be empty", + errorKey: "required" + }; + } $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index d2bf2cf87c..a493fffdd8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -12,4 +12,6 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 9106e2b334..37dea64c21 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -21,6 +21,11 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag fileManager.setFiles($scope.model.alias, []); //clear the current files $scope.files = []; + if ($scope.propertyForm.fileCount) { + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + } + } /** this method is used to initialize the data and to re-initialize it if the server value is changed */ @@ -71,6 +76,15 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag initialize(); + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one file selected) + $scope.validateMandatory = function () { + return { + isValid: !$scope.model.validation.mandatory || ((($scope.persistedFiles != null && $scope.persistedFiles.length > 0) || ($scope.files != null && $scope.files.length > 0)) && !$scope.clearFiles), + errorMsg: "Value cannot be empty", + errorKey: "required" + }; + } + //listen for clear files changes to set our model to be sent up to the server $scope.$watch("clearFiles", function (isCleared) { if (isCleared == true) { @@ -80,6 +94,8 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag else { //reset to original value $scope.model.value = $scope.originalValue; + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); } }); @@ -96,6 +112,10 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag $scope.files.push({ alias: $scope.model.alias, file: args.files[i] }); newVal += args.files[i].name + ","; } + + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + //set clear files to false, this will reset the model too $scope.clearFiles = false; //set the model value to be the concatenation of files selected. Please see the notes diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index afcba5a493..e200b2726c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -25,4 +25,7 @@
  • {{file.file.name}}
  • - + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index 4577430195..9c0d016189 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -12,31 +12,38 @@ angular.module("umbraco") $scope.isLoading = false; //load current value - $scope.currentTags = []; + if ($scope.model.value) { - if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") { - //it's a json array already - $scope.currentTags = $scope.model.value; - } - else { + if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { //it is csv if (!$scope.model.value) { - $scope.currentTags = []; + $scope.model.value = []; } else { - $scope.currentTags = $scope.model.value.split(","); + $scope.model.value = $scope.model.value.split(","); } } } + else { + $scope.model.value = []; + } + + // Method required by the valPropertyValidator directive (returns true if the property editor has at least one tag selected) + $scope.validateMandatory = function () { + return { + isValid: !$scope.model.validation.mandatory || ($scope.model.value != null && $scope.model.value.length > 0), + errorMsg: "Value cannot be empty", + errorKey: "required" + }; + } //Helper method to add a tag on enter or on typeahead select function addTag(tagToAdd) { - if (tagToAdd.length > 0) { - if ($scope.currentTags.indexOf(tagToAdd) < 0) { - $scope.currentTags.push(tagToAdd); - //update the model value, this is required if there's a server validation error, it can - // only then be cleared if the model changes - $scope.model.value = $scope.currentTags; + if (tagToAdd != null && tagToAdd.length > 0) { + if ($scope.model.value.indexOf(tagToAdd) < 0) { + $scope.model.value.push(tagToAdd); + //this is required to re-validate + $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); } } } @@ -47,7 +54,6 @@ angular.module("umbraco") if ($element.find('.tags-' + $scope.model.alias).parent().find(".tt-dropdown-menu .tt-cursor").length === 0) { //this is required, otherwise the html form will attempt to submit. e.preventDefault(); - $scope.addTag(); } } @@ -66,36 +72,26 @@ angular.module("umbraco") $scope.removeTag = function (tag) { - var i = $scope.currentTags.indexOf(tag); + var i = $scope.model.value.indexOf(tag); if (i >= 0) { - $scope.currentTags.splice(i, 1); - //update the model value, this is required if there's a server validation error, it can - // only then be cleared if the model changes - $scope.model.value = $scope.currentTags; + $scope.model.value.splice(i, 1); + //this is required to re-validate + $scope.propertyForm.tagCount.$setViewValue($scope.model.value.length); } }; - //sync model on submit, always push up a json array - $scope.$on("formSubmitting", function (ev, args) { - $scope.model.value = $scope.currentTags; - }); - //vice versa $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server $scope.model.value = newVal; - if ($scope.model.config.storageType && $scope.model.config.storageType === "Json") { - //it's a json array already - $scope.currentTags = $scope.model.value; - } - else { + if (!$scope.model.config.storageType || $scope.model.config.storageType !== "Json") { //it is csv if (!$scope.model.value) { - $scope.currentTags = []; + $scope.model.value = []; } else { - $scope.currentTags = $scope.model.value.split(","); + $scope.model.value = $scope.model.value.split(","); } } }; @@ -110,14 +106,14 @@ angular.module("umbraco") }); // remove current tags from the list return $.grep(tagList, function (tag) { - return ($.inArray(tag.value, $scope.currentTags) === -1); + return ($.inArray(tag.value, $scope.model.value) === -1); }); } // helper method to remove current tags function removeCurrentTagsFromSuggestions(suggestions) { return $.grep(suggestions, function (suggestion) { - return ($.inArray(suggestion.value, $scope.currentTags) === -1); + return ($.inArray(suggestion.value, $scope.model.value) === -1); }); } @@ -158,7 +154,6 @@ angular.module("umbraco") // name = the data set name, we'll make this the tag group name name: $scope.model.config.group, displayKey: "value", - //source: tagsHound.ttAdapter(), source: function (query, cb) { tagsHound.get(query, function (suggestions) { cb(removeCurrentTagsFromSuggestions(suggestions)); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index 1a0452e242..b1049a2309 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -1,21 +1,26 @@
    - -
    - Loading... + +
    + Loading...
    - + + + + + placeholder="@placeholders_enterTags" /> +
    +
    \ No newline at end of file