Simple and Complex Data Validation in Web Atoms
Tags: typescript, showdev, validation
Published: 2019-08-28 17:35:41

Data validation in User Interface is crucial for every application and Web Atoms makes it very easy to write.

## Simple Validation

Validation accessor is decorated with `@Validate` decorator and it is prefixed with the word `error`. You can bind these accessor in UI to display errors.

For example,

```typescript

export default SignupViewModel extends AtomViewModel {

    @Inject
    public navigationService: NavigationService;

    public model = {
        firstName: null,
        lastName: null
    };

    // both validate properties will return undefined value
    // unless `this.isValid` is referenced.

    @Validate
    public get errorFirstName(): string {
        return this.model.firstName ? "" : "First name is required";
    }

    @Validate
    public get errorLastName(): string {
        return this.model.firstName ? "" : "Last name is required";
    }

    public signup(): Promise<void> {

        // as soon as this property is called first time
        // validation decorator will update and error will be displayed
        if (!this.isValid) {
            await this.navigationService.alert(`Please enter required fields`);
            return;
        }

        // optional, if you want to reuse same form
        // you can call resetValidations to remove all errors
        this.resetValidations();
    }

}

```
TSX for Web
```typescript
class Component extends AtomControl {

   public viewModel: SignupViewModel;

   public create() {
       this.viewModel = this.resolve(SignupViewModel);
       this.render(
<div>
    <input 
        placeholder="First name:"
        value={Bind.twoWays(() => this.viewModel.model.firstName)}/>
    <span
        class="error"
        text={Bind.oneWay(()) => this.viewModel.errorFirstName}/>

    <input 
        placeholder="Last name:"
        value={Bind.twoWays(() => this.viewModel.model.lastName)}/>
    <span
        class="error"
        text={Bind.oneWay(()) => this.viewModel.errorLastName}/>

    ...

    <button
        eventClick={ () => this.viewModel.signup() }>Signup</button>

</div>
       );
   }

}
```

TSX for Xaml
```typescript
class Component extends AtomControl {

   public viewModel: SignupViewModel;

   public create() {
       this.viewModel = this.resolve(SignupViewModel);
       this.render(
<XF.StackLayout>
    <XF.Entry
        placeholder="First name:"
        text={Bind.twoWays(() => this.viewModel.model.firstName)}/>
    <XF.Label
        class="error"
        text={Bind.oneWay(()) => this.viewModel.errorFirstName}/>

    <XF.Entry 
        placeholder="Last name:"
        text={Bind.twoWays(() => this.viewModel.model.lastName)}/>
    <XF.Label
        class="error"
        text={Bind.oneWay(()) => this.viewModel.errorLastName}/>

    ...

    <XF.Button
        command={ () => this.viewModel.signup() }
        text="Signup"/>

</XF.StackLayout>
       );
   }

}

```

In above example, when page is loaded, error spans will not display anything. Even if `firstName` and `lastName` both are empty. As soon as user clicks `Signup` button, `this.isValid` get method will start watching for changes in all `@Validate` decorator methods and user interface will start displaying error message.

## Multi View Model Validation
Larger UI will need multiple smaller UI Components, in Web Atoms, you can easily create a UI with View Model that references parent view model, parent view model's validation extends to children and it return false for `isValid` even if children view models are not valid.

### Root Insurance View
```typescript
interface IInsurance  {
    id?: number;
    date?: Date;
    broker: string;
    type: string;
    applicants: IApplicant[];
}

export interface IApplicant {
    name: string;
    type: string;
    address?: string;
    city?: string;
}

export default class InsuranceViewModel extends AtomViewModel {

    @Inject
    public navigationService: NavigationService;

    public model: IInsurance = {
        broker: "",
        type: "General",
        applicants: [
            {
                name: "",
                type: "Primary"
            }
        ]
    };

    @Validate
    public get errorBroker(): string {
        return this.model.broker ? "" : "Broker cannot be empty";
    }

    public addApplicant(): void {
        this.model.applicants.add({
            name: "",
            type: "Dependent"
        });
    }

    public async save(): Promise<void> {
        if (!this.isValid) {
            await this.navigationService.alert("Please fix all errors", "Error");
            return;
        }
        await this.navigationService.alert("Save Successful", "Success");
    }

}
```
#### Insurance.html

We are displaying list of applicants in Insurance form, and we can add more applicants, note, each applicant's validation will be different based on type of applicant.

```typescript
export default class Insurance extends AtomControl {

   public create(): void {
      this.viewModel =  this.resolve(InsuranceViewModel) ;

      this.render(
      <div>
         <div>
            <input
               placeholder="Name"
               value={Bind.twoWays((x) => x.viewModel.model.broker)}>
            </input>
            <span
               style="color: red"
               text={Bind.oneWay((x) => x.viewModel.errorBroker)}>
            </span>
         </div>
         <AtomItemsControl
            items={Bind.oneTime((x) => x.viewModel.model.applicants)}>
            <AtomItemsControl.itemTemplate>
               <Applicant>
               </Applicant>
            </AtomItemsControl.itemTemplate>
         </AtomItemsControl>
         <button
            eventClick={Bind.event((x) => (x.viewModel).addApplicant())}>
            Add Applicant
         </button>
         <div>Other fields...</div>
         <button
            eventClick={Bind.event((x) => (x.viewModel).save())}>
            Save
         </button>
      </div>
      );
   }
}
```

### Nested Applicant View
Typescript
```typescript
export default class ApplicantViewModel extends AtomViewModel {

    @Inject
    public navigationService: NavigationService;

    public model: IApplicant;

    @Validate
    public get errorName(): string {
        return this.model.name ? "" : "Name cannot be empty";
    }

    @Validate
    public get errorAddress(): string {
        return this.model.address ? "" : "Address cannot be empty";
    }

    public async delete(): Promise<void> {
        if (!( await this.navigationService.confirm("Are you sure you want to delete this?") )) {
            return;
        }
        (this.parent as InsuranceViewModel).model.applicants.remove(this.model);
    }
}
```
#### Applicant.html

Applicant view is an independent view with its own view model, and it can also be used without parent list.

```typescript
export default class Applicant extends AtomControl {

   public create(): void {

       /** Following method will initialize and bind parent property of
        * ApplicantViewModel to InsuranceViewModel, this is specified in the form
        * of lambda so it will bind correctly after the control has been created
        * successfully.
        *
        * After parent is attached, parent view model will include all children validations
        * and will fail to validate if any of child is invalid
        */
      this.viewModel =  this.resolve(ApplicantViewModel, () => ({ model: this.data, parent: this.parent.viewModel })) ;

      this.render(
      <div
         style="margin: 5px; padding: 5px; border: solid 1px lightgray; border-radius: 5px">
         <div>
            <input
               placeholder="Name"
               value={Bind.twoWays((x) => x.viewModel.model.name)}>
            </input>
            <span
               style="color: red"
               text={Bind.oneWay((x) => x.viewModel.errorName)}>
            </span>
         </div>
         <div>
            <input
               placeholder="Address"
               value={Bind.twoWays((x) => x.viewModel.model.address)}>
            </input>
            <span
               style="color: red"
               text={Bind.oneWay((x) => x.viewModel.errorAddress)}>
            </span>
         </div>
         <button
            eventClick={Bind.event((x) => (x.viewModel).delete())}>
            Delete
         </button>
      </div>
      );
   }
}
```

> When a child view is created, we are assigning parent as visual parent's view model. So whenever this child view is invalid, even parent will be invalid.