DEV Community

loading...
Web Atoms

Simple and Complex Data Validation in Web Atoms

Akash Kava
Author of Web Atoms
Updated on ・4 min read

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,


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

}

Enter fullscreen mode Exit fullscreen mode

TSX for Web

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

}
Enter fullscreen mode Exit fullscreen mode

TSX for Xaml

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

}

Enter fullscreen mode Exit fullscreen mode

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

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

}
Enter fullscreen mode Exit fullscreen mode

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.

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>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

Nested Applicant View

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Applicant.html

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

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>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Forem Open with the Forem app