diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 8b3f51f0f9..1d100494f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -3,8 +3,81 @@ * @name umbraco.directives.directive:umbProperty * @restrict E **/ -angular.module("umbraco.directives") - .directive('umbProperty', function (userService, serverValidationManager, udiService) { +(function () { + 'use strict'; + + angular + .module("umbraco.directives") + .component('umbProperty', { + templateUrl: 'views/components/property/umb-property.html', + controller: UmbPropertyController, + controllerAs: 'vm', + transclude: true, + require: { + parentUmbProperty: '?^^umbProperty' + }, + bindings: { + property: "=", + elementUdi: "@", + // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) + propertyAlias: "@", + showInherit: "<", + inheritsFrom: "<" + } + }); + + + + function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) { + + const vm = this; + + vm.$onInit = onInit; + + vm.setPropertyError = function (errorMsg) { + vm.property.propertyErrorMessage = errorMsg; + }; + + vm.propertyActions = []; + vm.setPropertyActions = function (actions) { + vm.propertyActions = actions; + }; + + // returns the unique Id for the property to be used as the validation key for server side validation logic + vm.getValidationPath = function () { + + // the elementUdi will be empty when this is not a nested property + var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias; + vm.elementUdi = ensureUdi(vm.elementUdi); + return serverValidationManager.createPropertyValidationKey(propAlias, vm.elementUdi); + } + + vm.getParentValidationPath = function () { + if (!vm.parentUmbProperty) { + return null; + } + return vm.parentUmbProperty.getValidationPath(); + } + + function onInit() { + vm.controlLabelTitle = null; + if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if (u.allowedSections.indexOf("settings") !== -1 ? true : false) { + vm.controlLabelTitle = vm.property.alias; + } + }); + } + + vm.elementUdi = ensureUdi(vm.elementUdi); + + if (!vm.parentUmbProperty) { + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController"); + vm.parentUmbProperty = found ? found.vm : null; + } + } // if only a guid is passed in, we'll ensure a correct udi structure function ensureUdi(udi) { @@ -13,61 +86,6 @@ angular.module("umbraco.directives") } return udi; } + } - return { - scope: { - property: "=", - elementUdi: "@", - // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) - propertyAlias: "@", - showInherit: "<", - inheritsFrom: "<" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property.html', - link: function (scope, element, attr, ctrls) { - - scope.controlLabelTitle = null; - if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { - userService.getCurrentUser().then(function (u) { - if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { - scope.controlLabelTitle = scope.property.alias; - } - }); - } - - scope.elementUdi = ensureUdi(scope.elementUdi); - - }, - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope) { - - var self = this; - - //set the API properties/methods - - self.property = $scope.property; - self.setPropertyError = function (errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; - }; - - $scope.propertyActions = []; - self.setPropertyActions = function(actions) { - $scope.propertyActions = actions; - }; - - // returns the unique Id for the property to be used as the validation key for server side validation logic - self.getValidationPath = function () { - - // the elementUdi will be empty when this is not a nested property - var propAlias = $scope.propertyAlias ? $scope.propertyAlias : $scope.property.alias; - $scope.elementUdi = ensureUdi($scope.elementUdi); - return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi); - } - $scope.getValidationPath = self.getValidationPath; - - } - }; - }); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 6638ed4e6d..86ea94914a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -12,48 +12,88 @@ * Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will * be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. **/ -function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) { +function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = "show-validation"; var SAVING_EVENT_NAME = "formSubmitting"; var SAVED_EVENT_NAME = "formSubmitted"; + function notify(scope) { + scope.$broadcast("valStatusChanged", { form: scope.formCtrl }); + } + + function ValFormManagerController($scope) { + //This exposes an API for direct use with this directive + + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is + // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager + // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) + $scope.valFormManager = this; + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function (evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + this.showValidation = $scope.showValidation === true; + + this.notify = function () { + notify($scope); + } + + this.isValid = function () { + return !$scope.formCtrl.$invalid; + } + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + + /** + * Find's the valFormManager in the scope/DOM hierarchy + * @param {any} scope + * @param {any} ctrls + * @param {any} index + */ + function getAncestorValFormManager(scope, ctrls, index) { + + // first check the normal directive inheritance which relies on DOM inheritance + var found = ctrls[index]; + if (found) { + return found; + } + + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController"); + return found ? found.valFormManager : null; + } + return { require: ["form", "^^?valFormManager", "^^?valSubView"], restrict: "A", - controller: function($scope) { - //This exposes an API for direct use with this directive - - var unsubscribe = []; - var self = this; - - //This is basically the same as a directive subscribing to an event but maybe a little - // nicer since the other directive can use this directive's API instead of a magical event - this.onValidationStatusChanged = function (cb) { - unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { - cb.apply(self, [evt, args]); - })); - }; - - this.showValidation = $scope.showValidation === true; - - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, + controller: ValFormManagerController, link: function (scope, element, attr, ctrls) { function notifySubView() { - if (subView){ + if (subView) { subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation }); } } - var formCtrl = ctrls[0]; - var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null; + var formCtrl = scope.formCtrl = ctrls[0]; + var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; @@ -81,14 +121,14 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var validatorLengths = _.map(formCtrl.$error, function (val, key) { // if there are child ng-forms, include the $error collections in those as well var innerErrorCount = _.reduce( - _.map(val, v => - _.reduce( - _.map(v.$error, e => e.length), - (m, n) => m + n - ) - ), - (memo, num) => memo + num - ); + _.map(val, v => + _.reduce( + _.map(v.$error, e => e.length), + (m, n) => m + n + ) + ), + (memo, num) => memo + num + ); return val.length + innerErrorCount; }); //sum up all numbers in the resulting array @@ -98,8 +138,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //this is the value we watch to notify of any validation changes on the form return sum; }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); + notify(scope); + notifySubView(); //find all invalid elements' .control-group's and apply the error class @@ -128,7 +169,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var unsubscribe = []; //listen for the forms saving event - unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; notifySubView(); @@ -137,7 +178,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location })); //listen for the forms saved event - unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = false; @@ -151,7 +192,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { + var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) { var infiniteEditors = editorService.getEditors(); @@ -178,10 +219,10 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location "disableEscKey": true, "submitButtonLabel": labels.stayButton, "closeButtonLabel": labels.discardChangesButton, - submit: function() { + submit: function () { overlayService.close(); }, - close: function() { + close: function () { // close all editors editorService.closeAll(); // allow redirection @@ -190,7 +231,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var parts = nextPath.split("?"); var query = {}; if (parts.length > 1) { - _.each(parts[1].split("&"), function(q) { + _.each(parts[1].split("&"), function (q) { var keyVal = q.split("="); query[keyVal[0]] = keyVal[1]; }); @@ -215,13 +256,13 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(locationEvent); //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { + scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); } }); - $timeout(function(){ + $timeout(function () { formCtrl.$setPristine(); }, 1000); 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 27fa67695b..30ba459baf 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 @@ -8,24 +8,26 @@ * We will listen for server side validation changes * and when an error is detected for this property we'll show the error message. * In order for this directive to work, the valFormManager directive must be placed on the containing form. +* We don't set the validity of this validator to false when client side validation fails, only when server side +* validation fails however we do respond to the client side validation changes to display error and adjust UI state. **/ -function valPropertyMsg(serverValidationManager, localizationService) { +function valPropertyMsg(serverValidationManager, localizationService, angularHelper) { return { - require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'], + require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'], replace: true, restrict: "E", template: "
{{ getValidationPath() }}
+ {{ vm.getValidationPath() }}