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:
Shannon
2020-07-17 00:48:28 +10:00
parent 5c91774ae9
commit d01b06bd9c
8 changed files with 129 additions and 63 deletions

View File

@@ -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
));
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -39,8 +39,6 @@
}
&.--error {
color: @ui-active-type;
border-color: @ui-active;
background-color: @ui-active;
border-color: @formErrorBorder !important;
}
}

View File

@@ -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>