Simple and Complex Data Validation in Web Atoms
Akash Kava
Posted on August 28, 2019
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();
}
}
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>
);
}
}
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>
);
}
}
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");
}
}
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>
);
}
}
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);
}
}
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>
);
}
}
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.
Posted on August 28, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.