only call model state callbacks once instead of exponentially, fixes valPropertyMsg, no more watching the value, uses clever trick of assigning $validators, now works for nested content because we don't need to watch values, makes nested content look more like block editor, adds error borders
This commit is contained in:
@@ -83,7 +83,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
|
||||
return labels.propertyHasErrors;
|
||||
}
|
||||
|
||||
// return true if there is only a single error left on the property form of either valPropertyMsg or valServer
|
||||
// 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() {
|
||||
|
||||
var errCount = angularHelper.countAllFormErrors(formCtrl);
|
||||
@@ -135,31 +136,55 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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 (ctrl.hasOwnProperty('$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 there's not already a watch
|
||||
|
||||
if (!watcher) {
|
||||
watcher = scope.$watch("currentProperty.value",
|
||||
function (newValue, oldValue) {
|
||||
if (angular.equals(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkAndClearError()) {
|
||||
resetError();
|
||||
}
|
||||
else if (showValidation && scope.errorMsg === "") {
|
||||
formCtrl.$setValidity('valPropertyMsg', false, formCtrl);
|
||||
scope.errorMsg = getErrorMsg();
|
||||
}
|
||||
}, true);
|
||||
watcher = scope.$watchCollection(
|
||||
() => formCtrl,
|
||||
function (updatedFormController) {
|
||||
var ngModels = [];
|
||||
collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels);
|
||||
ngModels.forEach(x => {
|
||||
if (!x.$validators.serverValidityResetter) {
|
||||
x.$validators.serverValidityResetter = resetServerValidityValidator(x);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,11 +199,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
|
||||
function resetError() {
|
||||
stopWatch();
|
||||
hasError = false;
|
||||
formCtrl.$setValidity('valPropertyMsg', true, formCtrl);
|
||||
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
|
||||
@@ -270,13 +297,22 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
|
||||
// the correct field validation in their property editors.
|
||||
|
||||
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
|
||||
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, formCtrl);
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
startWatch();
|
||||
|
||||
|
||||
if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) {
|
||||
var propertyValidationPath = umbPropCtrl.getValidationPath();
|
||||
console.log("only 1 left, clearing! " + propertyValidationPath);
|
||||
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
|
||||
resetError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
resetError();
|
||||
@@ -289,7 +325,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
|
||||
"",
|
||||
serverValidationManagerCallback,
|
||||
currentSegment,
|
||||
{ matchType: "suffix" } // match property validation path prefix
|
||||
{ matchType: "prefix" } // match property validation path prefix
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ function valServerMatch(serverValidationManager) {
|
||||
//subscribe to the server validation changes
|
||||
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
|
||||
if (!isValid) {
|
||||
formCtrl.$setValidity('valServerMatch', false, formCtrl);
|
||||
formCtrl.$setValidity('valServerMatch', false);
|
||||
}
|
||||
else {
|
||||
formCtrl.$setValidity('valServerMatch', true, formCtrl);
|
||||
formCtrl.$setValidity('valServerMatch', true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,13 @@ function angularHelper($q) {
|
||||
}
|
||||
keys.forEach(validationKey => {
|
||||
var ctrls = formCtrl.$error[validationKey];
|
||||
ctrls.forEach(ctrl => {
|
||||
if (isForm(ctrl)) {
|
||||
ctrls.forEach(ctrl => {
|
||||
if (!ctrl) {
|
||||
// this happens when $setValidity('err', true) is called on a form controller without specifying the 3rd parameter for the control/form
|
||||
// which just means that this is an error on the formCtrl itself
|
||||
allErrors.push(formCtrl); // add the error
|
||||
}
|
||||
else if (isForm(ctrl)) {
|
||||
// sometimes the control in error is the same form so we cannot recurse else we'll cause an infinite loop
|
||||
// and in this case it means the error is assigned directly to the form, not a control
|
||||
if (ctrl === formCtrl) {
|
||||
|
||||
@@ -382,9 +382,7 @@ function serverValidationManager($timeout) {
|
||||
fieldName: fieldName,
|
||||
errorMsg: errorMsg
|
||||
});
|
||||
}
|
||||
|
||||
notifyCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,6 +444,7 @@ function serverValidationManager($timeout) {
|
||||
throw "modelState is not an object";
|
||||
}
|
||||
|
||||
var hasPropertyErrors = false;
|
||||
for (const [key, value] of Object.entries(modelState)) {
|
||||
|
||||
//This is where things get interesting....
|
||||
@@ -506,6 +505,7 @@ function serverValidationManager($timeout) {
|
||||
|
||||
// add a generic error for the property
|
||||
addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment);
|
||||
hasPropertyErrors = true;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -514,6 +514,11 @@ function serverValidationManager($timeout) {
|
||||
addFieldError(key, value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPropertyError) {
|
||||
// ensure all callbacks are called after property errors are added
|
||||
notifyCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
function createPropertyValidationKey(propertyAlias, parentValidationPath) {
|
||||
@@ -718,7 +723,11 @@ function serverValidationManager($timeout) {
|
||||
|
||||
getVariantCallbacks: getVariantCallbacks,
|
||||
addFieldError: addFieldError,
|
||||
addPropertyError: addPropertyError,
|
||||
|
||||
addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) {
|
||||
addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment);
|
||||
notifyCallbacks(); // ensure all callbacks are called
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
position: relative;
|
||||
text-align: left;
|
||||
background: @white;
|
||||
border: 1px solid @gray-9;
|
||||
border-radius: @baseBorderRadius;
|
||||
transition: border-color 120ms;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
|
||||
&.--error {
|
||||
border-color: @formErrorBorder !important;
|
||||
}
|
||||
}
|
||||
|
||||
.umb-nested-content__item.ui-sortable-placeholder {
|
||||
@@ -54,7 +63,7 @@
|
||||
}
|
||||
|
||||
.umb-nested-content__header-bar {
|
||||
border-bottom: 1px solid @gray-9;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: @white;
|
||||
|
||||
@@ -207,9 +216,9 @@
|
||||
|
||||
.umb-nested-content__content {
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid @gray-9;
|
||||
border-left: 1px solid @gray-9;
|
||||
border-right: 1px solid @gray-9;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@
|
||||
}
|
||||
|
||||
&.--error {
|
||||
color: @ui-active-type;
|
||||
border-color: @ui-active;
|
||||
background-color: @ui-active;
|
||||
border-color: @formErrorBorder !important;
|
||||
}
|
||||
}
|
||||
.blockelement-inlineblock-editor__inner {
|
||||
|
||||
@@ -39,8 +39,6 @@
|
||||
}
|
||||
|
||||
&.--error {
|
||||
color: @ui-active-type;
|
||||
border-color: @ui-active;
|
||||
background-color: @ui-active;
|
||||
border-color: @formErrorBorder !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,30 +6,41 @@
|
||||
|
||||
<div class="umb-nested-content__items" ng-hide="vm.nodes.length === 0" ui-sortable="vm.sortableOptions" ng-model="vm.nodes">
|
||||
|
||||
<div class="umb-nested-content__item" ng-repeat="node in vm.nodes" ng-class="{ 'umb-nested-content__item--active' : vm.currentNode.key === node.key, 'umb-nested-content__item--single' : vm.singleMode }">
|
||||
<div ng-repeat="node in vm.nodes">
|
||||
|
||||
<div class="umb-nested-content__header-bar" ng-click="vm.editNode($index)" ng-hide="vm.singleMode" umb-auto-focus="{{vm.currentNode.key === node.key ? 'true' : 'false'}}">
|
||||
<ng-form name="ncRowForm" val-server-match="{ 'contains' : node.key }">
|
||||
|
||||
<div class="umb-nested-content__heading"><i ng-if="vm.showIcons" class="icon" ng-class="vm.getIcon($index)"></i><span class="umb-nested-content__item-name" ng-class="{'--has-icon': vm.showIcons}" ng-bind="vm.getName($index)"></span></div>
|
||||
<div class="umb-nested-content__item"
|
||||
ng-class="{ 'umb-nested-content__item--active' : vm.currentNode.key === node.key, 'umb-nested-content__item--single' : vm.singleMode, '--error': ncRowForm.$invalid }">
|
||||
|
||||
<div class="umb-nested-content__icons">
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--copy" title="{{vm.labels.copy_icon_title}}" ng-click="vm.clickCopy($event, node);" ng-if="vm.showCopy">
|
||||
<i class="icon icon-documents" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{vm.labels.copy_icon_title}}</span>
|
||||
</button>
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-class="{ 'umb-nested-content__icon--disabled': !vm.canDeleteNode($index) }" ng-click="vm.requestDeleteNode($index); $event.stopPropagation();">
|
||||
<i class="icon icon-trash" aria-hidden="true"></i>
|
||||
<span class="sr-only">
|
||||
<localize key="general_delete">Delete</localize>
|
||||
</span>
|
||||
</button>
|
||||
<div class="umb-nested-content__header-bar"
|
||||
ng-click="vm.editNode($index)"
|
||||
ng-hide="vm.singleMode"
|
||||
umb-auto-focus="{{vm.currentNode.key === node.key ? 'true' : 'false'}}">
|
||||
|
||||
<div class="umb-nested-content__heading"><i ng-if="vm.showIcons" class="icon" ng-class="vm.getIcon($index)"></i><span class="umb-nested-content__item-name" ng-class="{'--has-icon': vm.showIcons}" ng-bind="vm.getName($index)"></span></div>
|
||||
|
||||
<div class="umb-nested-content__icons">
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--copy" title="{{vm.labels.copy_icon_title}}" ng-click="vm.clickCopy($event, node);" ng-if="vm.showCopy">
|
||||
<i class="icon icon-documents" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{vm.labels.copy_icon_title}}</span>
|
||||
</button>
|
||||
<button type="button" class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-class="{ 'umb-nested-content__icon--disabled': !vm.canDeleteNode($index) }" ng-click="vm.requestDeleteNode($index); $event.stopPropagation();">
|
||||
<i class="icon icon-trash" aria-hidden="true"></i>
|
||||
<span class="sr-only">
|
||||
<localize key="general_delete">Delete</localize>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="umb-nested-content__content" ng-if="vm.currentNode.key === node.key && !vm.sorting">
|
||||
<umb-nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="umb-nested-content__content" ng-if="vm.currentNode.key === node.key && !vm.sorting">
|
||||
<umb-nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user