Advertisement

#20 Processing the ModelState Errors

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.

You can view a completed example of the authenticator component here.

Processing model state errors

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts

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);
    }
}
(a) Adding a private _modelErrors field to our class and setting its properties using a new xhrFailure method that we will use whenever we have a request failure that we need to handle.

With those changes made if we initiate a login request we should see our _modelErrors field displayed in the console (b).

Console output showing that we are settings the properties of our
            _modelErrors field correctly.
(b) Console output showing that we are settings the properties of our _modelErrors field correctly.

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

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}}
        ...
(c) Modification of our info mixin within the template that will display the model state errors for a particular control, if any exists.

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);
    }
}
(d) Styling for our new model state error elements.

authenticator.component.ts (2)

import loDashEach = require("lodash/each");
...
export class AuthenticatorComponent implements ... {
    ...
    public get modelErrors() {
        return this._modelErrors;
    }
    ...
}
(e) We need to create a public getter for us to be able to display the model state errors.

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).

Displaying of the model state errors within our user interface.
(f) Displaying of the model state errors within our user interface.

I don't like the length error message

  • WebUi
    • Models
      • Account
        • AccountLoginRequest.cs

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; }
    }
}
(g) Fixing the requirements for the length of the username by splitting the StringLength into two separate attributes.
Advertisement

Displaying the names of invalid controls on the default info page

  • WebUi
    • Models
      • Source
        • components
          • authenticator
            • authenticator.component.pug
            • authenticator.component.ts

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)
(h) Simple modification to the template file in order to be uniform in the way that we create info pages.

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);
        });
    }
 }
(i) Modification of the typescript class that will result in the keys of the model state errors being displayed on the default info page.

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).

Image showing the keys for the model state errors being displayed on
            the default info page.
(j) Image showing the keys for the model state errors being displayed on the default info page.

How about a validator for model state errors

  • WebUi
    • Source
      • app
        • vendor.ts
      • components
        • authenticator
          • authenticator.component.pug
          • authenticator.component.ts

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}')`, ...) ...
...
(k) Change to our template that will allow us to disable the buttons based on the result of a method call.

vendor.ts (1)

...
import "lodash/isNil";
...
(l) Importing of the isNil function because I'm lazy.

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,
            () => {

            },
            () => {
                ...
            });
    }
    ...
}
(e) With these changes the send button will now be disabled if the form is invalid and our form controls are now aware of the model state errors when determining validity.

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.

(m)
(n)

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

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();
    }
}
(e) Updating our form controls so that their validation is skipped under the appropriate conditions and forcing the update of the value and validities of all of our controls.

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) .

When attempting to login we now only have to provide a username
            and password for the form to be valid.
(o) When attempting to login we now only have to provide a username and password for the form to be valid.
When a response is returned that contains model state errors the controls with
            errors now show that they are invalid.
(p) When a response is returned that contains model state errors the controls with errors now show that they are invalid.
Exciton Interactive LLC
Advertisement