Here is the solution we've implemented. It is a bit of a hack and a bit of a workaround. I hope someone has something better than this.
We added a required number column called ErrorCount and hid it on the forms using csr. So the all the html for the column is on the form, but it is hidden from the user.
On the form we use code from this thread to capture the original click handler from the save button and save it in a different function and trigger our custom validation function on click. Like this:
var originalSaveButtonClickHandler = function(){};
$(document).ready( function () {
var saveButton = $("[name$='diidIOSaveItem']")
if (saveButton.length > 0) {
originalSaveButtonClickHandler = saveButton[0].onclick;
}
$(saveButton).attr("onclick", "ValidateForm()");
});
This calls ValidateForm when the user clicks and that function, among other things, clears any value in the required ErrorCount field, submits the form to SharePoint validation, and then counts the errors on the form. Using this code.
$('input[title="ErrorCount Required Field"]').val('');
var b = SPClientForms.ClientFormManager.SubmitClientForm(formUniqueId);
SubmitClientForm() always returns true, so it can't be used to determine if the form is actually validated and if the are no errors it will just proceed along, save, and close the form.
But, there will always be at least one error, because ErrorCount is required, has just been cleared, and is hidden from the user. So that stops the form from actually submitting. We then use jQuery to check if there are any SharePoint errors on the page. If there is only one we know it is the hidden ErrorCount field and that the form is actually good and we can proceed with what we need to do, fill in a value for the required ErrorCount column, and finally submit. Here is the code for that part.
// find any empty required fields and any managed metadata that are invalid
var $errs = $('span[id^="Error_"],span.invalid-text,span.sp-peoplepicker-errorMsg');
if ($errs.length===1){
// there is only one error which is the hidden error field
// so we are good to proceed
$('input[title="ErrorCount Required Field"]').val(0);
MyForms.launchWorkflow()
.done(function(){
originalSaveButtonClickHandler();
});
}
In that selector, span[id^="Error_"] finds most of the "You can't leave this blank" or other validation warnings, span.invalid-text finds any taxonomy fields that have invalid terms in the field, but are otherwise filled in, and span.sp-peoplepicker-errorMsg finds any people pickers that have something wrong with them.
The MyForms.launchWorkflow() is an asynchronous ajax call to start a workflow on the item and do a few other things behind the scenes before the item is actually saved. Once it is done, then we call the original save function that we saved earlier, and because the we have filled in the ErrorCount column it will actually save.