#20 Processing the ModelState Errors
Thursday, February 15, 2018
In this article we take the model state errors that we are receiving from the server and display them to the user. On the default info screen we will display the names of any controls that have an error and when the user focuses a particular control we will display that controls errors. Once that is complete we will configure our component to only allow sending a request to the server once the form is in a valid state. Lastly we will configure the validation for our controls to only run when appropriate.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Processing model state errors
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
Now that we are receiving and processing model state errors with our authenticator http service we need to do
something with them in our component class. Since we will need to do this with more requests and not just when
the user is trying to login we will create a xhrFailure
method
that we can use for all request failures (a). This method will
process the model state errors on our service by creating objects, indexed by control key, as properties on
a private _modelErrors
field.
authenticator.component.ts (1)
import loDashEach = require("lodash/each");
...
export class AuthenticatorComponent implements ... {
...
private _modelErrors: { [key: string]: { errors: string[]; } } = {};
...
private login = () => {
this._authenticatorHttpService.login(
{
...
},
() => {
},
() => {
this.xhrFailure();
});
}
...
private xhrFailure = () => {
this._modelErrors = {};
loDashEach(this._authenticatorHttpService.modelStateErrors, e => {
const controlKey = `control${e.key}`;
this._modelErrors[controlKey] = {
errors: e.value
}
});
console.log(this._modelErrors);
}
}
With those changes made if we initiate a login request we should see our _modelErrors
field
displayed in the console (b).
Now that we are sure that part is working we can remove the console.log(this._modelErrors);
statement.
Displaying the model state errors to the user
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
Knowing what the model state errors are doesn't do us much good if the user doesn't know what they are. In order to display them to the user we need to modify our template as shown in (c). Here we are creating an unordered list that will be displayed if a particular control has an error. The list elements will just be the errors that the server as sent in the response to the request. Next we need to add just a little bit of styling to the ui elements for our errors (d). As the modifications of our template shows we need to create a property on the component class for getting the model errors (e).
authenticator.component.pug (1)
...
mixin info(...)
div.info(...)
...
ul.model-errors(*ngIf=`modelErrors['${key}']`)
li(*ngFor=`let error of modelErrors['${key}'].errors`) {{error}}
...
authenticator.component.scss (2)
.authenticator {
.model-errors {
border: 1px solid black;
border-left-width: 0;
border-right-width: 0;
color: $invalid-color;
padding: em(10px);
background-color: #2b2b2b;
list-style-type: disc;
padding-left: em(30px);
}
}
authenticator.component.ts (2)
import loDashEach = require("lodash/each");
...
export class AuthenticatorComponent implements ... {
...
public get modelErrors() {
return this._modelErrors;
}
...
}
Again we need to send a request to the server so that we can receive the model state errors. When that is done if we focus the username input we can see the model state errors being displayed by our ui (f).
I don't like the length error message
-
WebUi
-
Models
-
Account
- AccountLoginRequest.cs
-
Account
-
Models
The error message that is being sent by due to the username being less than the required number of characters just does not fit in with the rest of the validators. To fix this we just need to split the requirement for the min and max character lengths into two different attributes (g).
AccountLoginRequest.cs (1)
...
namespace WebUi.Models.Account
{
public class AccountLoginRequest
{
...
[MaxLength(100, ErrorMessage = "Maximum length is 100")]
[MinLength(8, ErrorMessage = "Minimum length is 8")]
public string Username { get; set; }
}
}
Displaying the names of invalid controls on the default info page
-
WebUi
-
Models
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.ts
-
authenticator
-
components
-
Source
-
Models
As it stands now if there is an error response from the server the user is not notified about which controls
are invalid unless they go through and focus each of the inputs. This is of course not the best user
experience. We will address this by displaying with controls have an error on the default info page. To
do this we will first modify the template by changing the call to the info mixin made for the default page
by creating a controlDefaultKey
variable and passing it into the mixin
(h). For uniformity sake we should have done this when first creating the info page.
With the change to the template we need to also change the typescript class by adding a getter for
the controlDefaultInfoId
and
the controlDefaultKey
(i). Just be sure the key has the same value as the template variable. After that is
done we also need to modify the statement within the ngAfterViewInit
that
sets the default info DOM. Finally we need to add the keys from the model state errors that we received in the
xhrFailure
method.
authenticator.component.pug (2)
...
- var controlDefaultKey = "controlDefault";
...
div.authenticator
...
div.info-container
div.info-pages
+info("Welcome!", controlDefaultKey)
authenticator.component.ts (3)
...
export class AuthenticatorComponent ... {
...
public get controlDefaultInfoId() { return "info-default"; }
...
public get controlDefaultKey() { return "controlDefault"; }
...
public ngAfterViewInit(): void {
...
this._defaultInfoDOM = this._domReader.findChildById(this._elementRef.nativeElement, this.controlDefaultInfoId);
...
}
...
private xhrFailure = () => {
...
this._modelErrors[this.controlDefaultKey] = {
errors: []
};
loDashEach(this._authenticatorHttpService.modelStateErrors, e => {
...
this._modelErrors[this.controlDefaultKey].errors.push(e.key);
});
}
}
Again once those changes are made and a request is sent to the server we should now see the keys for the errors being displayed on the default info page (j).
How about a validator for model state errors
-
WebUi
-
Source
-
app
- vendor.ts
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.ts
-
authenticator
-
app
-
Source
Now that we are displaying the errors to the user it would be nice that our form behaved like it had some
idea about them. To do this we will start by setting whether or not the buttons should be disabled using
the result of a method call (k). Next I have avoid adding the
isNil
function from lodash in a miss guided attempt to keep my
usage of the library as small as possible. But I have reached the point where I am tired of checking the
typeof something as well as if it is null so here comes the importing of the
isNil
function (l). As far as determining
whether a button is disabled all we care about, at least for now, is the send button and it will be
enabled/disabled based on the validity of the form (e). Next we
just need to add a validator to all of our form controls except for the remember me control that will
check if there is a model state error for that control. We will also keep a reference of the data that was sent
with the request so that we can say that if the user tries to use the same information that it is invalid
without sending the request again.
authenticator.component.put (3)
...
mixin button(...)
button(..., [disabled]=`isButtonDisabled('${key}')`, ...) ...
...
vendor.ts (1)
...
import "lodash/isNil";
...
authenticator.component.ts (2)
...
import loDashIsNil = require("lodash/isNil");
...
export class AuthenticatorComponent implements ... {
...
private _xhrData: any = null;
...
public isButtonDisabled = (key: string) => {
switch (key) {
case this.buttonSendKey:
return this.authForm.invalid;
default:
return false;
}
}
...
private createFormGroup = () => {
...
const getServerErrorValidator = (controlKey: string, xhrDataKey: string) => {
return {
validator: XcValidators.userDefined,
config: {
error: "servererror",
pass: () => {
if (loDashIsNil(this._modelErrors) || loDashIsNil(this._modelErrors[controlKey])) {
return true;
}
if (loDashIsNil(this._xhrData) || loDashIsNil(this._xhrData[xhrDataKey])) {
return true;
}
return this.authForm.controls[controlKey].value !== this._xhrData[xhrDataKey];
}
}
};
};
controls[this.controlEmailKey] = ["",
XcValidators.compose({
...
"serverError": getServerErrorValidator(this.controlEmailKey, "email")
})
];
...
controls[this.controlPasswordKey] = ["",
XcValidators.compose({
...
"serverError": getServerErrorValidator(this.controlPasswordKey, "password")
})
];
...
controls[this.controlUsernameKey] = ["",
XcValidators.compose({
...
"serverError": getServerErrorValidator(this.controlUsernameKey, "username")
})
];
...
this.controlEmailConfirm.setValidators(XcValidators.compose({
...
"serverError": getServerErrorValidator(this.controlEmailConfirmKey, "emailConfirm")
}));
this.controlPasswordConfirm.setValidators(XcValidators.compose({
...
"serverError": getServerErrorValidator(this.controlPasswordConfirmKey, "passwordConfirm")
}));
}
...
private login = () => {
this._xhrData = {
password: this.controlPassword.value,
rememberme: this.controlRememberMe.value,
username: this.controlUsername.value
};
this._authenticatorHttpService.login(this._xhrData,
() => {
},
() => {
...
});
}
...
}
With those changes the send button is now disabled if the form is invalid (m). Once the form is valid the send button is able to be pressed once again.
There are of course two things we need to fix now: (1) we had to type valid entries into all of the inputs, (2) after a response is returned with errors all of the controls still show they are valid. Time to take care of these issues.
Skipping validation and updating validities
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
Back several articles ago when we created our validators and more specifically when we created the compose method we
added the ability to say whether the validators for a particular control should be skipped or not. We have not used
the ability until now. In (e) we are modifying the creation of our
controls in the createFormGroup
method to specify when the validation for
each control should be skipped. We also need to make one change to the xhrFailure
to force the updating of the value and validity of each of the controls within our form.
authenticator.component.ts (2)
...
export class AuthenticatorComponent implements ... {
...
private createFormGroup = () => {
...
controls[this.controlEmailKey] = ["",
XcValidators.compose(...,
() => {
return this.isLogin && this.isSubActionForgotPassword === false && this.isSubActionForgotUsername === false;
}
)
];
...
controls[this.controlPasswordKey] = ["",
XcValidators.compose({...,
() => {
return this.isLogin && (this.isSubActionForgotPassword || this.isSubActionForgotUsername);
}
)
];
...
controls[this.controlUsernameKey] = ["",
XcValidators.compose(...,
() => {
return this.isSubActionForgotPassword || this._isSubActionForgotUsername;
}
)
];
...
this.controlEmailConfirm.setValidators(XcValidators.compose(
...,
() => { return this.isLogin }
));
this.controlPasswordConfirm.setValidators(XcValidators.compose(
...,
() => { return this.isLogin }
));
}
...
private xhrFailure = () => {
...
this._formController.updateAllValuesAndValidities();
}
}
With those changes made when we are attempting to login the only controls that we are required to use are the username and password (o). In addition to that when a request is sent and model state errors are returned the contols with errors now show that they are invalid (p) .