import { Disposable, dom, Observable, styled } from 'grainjs'; import { colors } from 'app/client/ui2018/cssVars'; /** * Simple validation controls. Renders as a red text with a validation message. * * Sample usage: * * const group = new ValidationGroup(); * async function save() { * if (await group.validate()) { * api.save(....) * } * } * .... * dom('div', * dom('Login', 'Enter login', input(login), group.resetInput()), * dom.create(Validator, accountGroup, 'Login is required', () => Boolean(login.get()) === true)), * dom.create(Validator, accountGroup, 'Login must by unique', async () => await throwsIfLoginIsTaken(login.get())), * dom('button', dom.on('click', save)) * ) */ /** * Validation function. Can return either boolean value or throw an error with a message that will be displayed * in a validator instance. */ type ValidationFunction = () => (boolean | Promise | void | Promise) /** * Validation groups allow you to organize validator controls on a page as a set. * Each validation group can perform validation independently from other validation groups on the page. */ export class ValidationGroup { // List of attached validators. private _validators: Validator[] = []; /** * Runs all validators check functions. Returns result of the validation. */ public async validate() { let valid = true; for (const val of this._validators) { try { const result = await val.check(); // Validator can either return boolean, Promise or void. Booleans are straightforwards. // When validator has a void/Promise result it means that it just asserts certain invariant, and should // throw an exception when this invariant is not met. Error message can be used to amend the message in the // validator instance. const isValid = typeof result === 'boolean' ? result : true; val.set(isValid); if (!isValid) { valid = false; break; } } catch (err) { valid = false; val.set((err as Error).message); break; } } return valid; } /** * Attaches single validator instance to this group. Validator can be in multiple groups * at the same time. */ public add(validator: Validator) { this._validators.push(validator); } /** * Helper that can be attached to the input element to reset validation status. */ public inputReset() { return dom.on('input', this.reset.bind(this)); } /** * Reset all validators statuses. */ public reset() { this._validators.forEach(val => val.set(true)); } } /** * Validator instance. When triggered shows a red text with an error message. */ export class Validator extends Disposable { private _isValid = Observable.create(this, true); private _message = Observable.create(this, ''); constructor(public group: ValidationGroup, message: string, public check: ValidationFunction) { super(); group.add(this); this._message.set(message); } /** * Helper that can be attached to the input element to reset validation status. */ public inputReset() { return dom.on('input', this.set.bind(this, true)); } /** * Sets the validation status. If isValid is a string it is treated as a falsy value, and will * mark this validator as invalid. */ public set(isValid: boolean | string) { if (this.isDisposed()) { return; } if (typeof isValid === 'string') { this._message.set(isValid); this._isValid.set(!isValid); } else { this._isValid.set(isValid ? true : false); } } public buildDom() { return cssError( dom.text(this._message), dom.hide(this._isValid), ); } } const cssError = styled('div.validator', ` color: ${colors.error}; `);