UNPKG

powerform

Version:
350 lines (345 loc) 11 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Powerform = {})); })(this, (function (exports) { 'use strict'; class DecodeError extends Error { } function optional(decoder) { return (val) => { if (val === "") return [undefined, ""]; return decoder(val); }; } class Field { constructor(decoder, ...validators) { this.decoder = decoder; this.fieldName = ""; this._error = ""; // html input field value is always string no matter // what its type is, type is only for UI this.initialValue = '""'; this.previousValue = '""'; this.currentValue = '""'; this.validators = validators; } optional() { const optionalDecoder = optional(this.decoder); return new Field(optionalDecoder, ...this.validators); } // sets initial values initValue(val) { val = typeof val === "string" ? val : JSON.stringify(val); this.setValue(val, true); this.makePristine(); return this; } onInput(i) { this.inputHandler = i; return this; } onError(i) { this.errorHandler = i; return this; } onChange(c) { this.changeHandler = c; return this; } triggerOnError() { const callback = this.errorHandler; callback && callback(this.error); if (this.form) this.form.triggerOnError(); } triggerOnChange() { const callback = this.changeHandler; callback && callback(this.raw); this.form && this.form.triggerOnChange(); } setValue(val, skipTrigger) { const strVal = JSON.stringify(val); if (this.currentValue === strVal) return; this.previousValue = this.currentValue; // input handlers should deal with actual value // not a strigified version this.currentValue = JSON.stringify(this.inputHandler ? this.inputHandler(val, this.previousValue) : val); if (skipTrigger) return; this.triggerOnChange(); } get raw() { return JSON.parse(this.currentValue); } get value() { const [val, err] = this.decoder(JSON.parse(this.currentValue)); if (err !== "") throw new DecodeError(`Invalid value at ${this.fieldName}`); return val; } _validate() { const [parsedVal, err] = this.decoder(JSON.parse(this.currentValue)); if (err !== "") { return err; } if (parsedVal === undefined) return; const [preValue] = this.decoder(this.previousValue); if (preValue === undefined) return; for (const v of this.validators) { const err = v(parsedVal, { prevValue: preValue, fieldName: this.fieldName, // optimise this step all: this.form ? this.form.raw : {}, }); if (err != undefined) { return err; } } return undefined; } validate() { const err = this._validate(); if (err === undefined) { this.setError(""); return true; } this.setError(err); return false; } isValid() { const err = this._validate(); return !err; } setError(error, skipTrigger) { if (this._error === error) return; this._error = error; if (skipTrigger) return; this.triggerOnError(); } get error() { return this._error; } isDirty() { return this.previousValue !== this.currentValue; } makePristine() { this.initialValue = this.previousValue = this.currentValue; this.setError(""); } reset() { this.setValue(JSON.parse(this.initialValue)); this.makePristine(); } setAndValidate(value) { this.setValue(value); this.validate(); return this.error; } } const defaultConfig = { multipleErrors: false, stopOnError: false, }; class Form { constructor(fields, config = defaultConfig) { this.fields = fields; this.config = config; this.getNotified = true; for (const fieldName in fields) { fields[fieldName].form = this; fields[fieldName].fieldName = fieldName; } } initValue(values) { for (const fieldName in this.fields) { this.fields[fieldName].initValue(values[fieldName]); } return this; } onError(handler) { this.errorHandler = handler; return this; } onChange(handler) { this.changeHandler = handler; return this; } toggleGetNotified() { this.getNotified = !this.getNotified; } setValue(data, skipTrigger) { this.toggleGetNotified(); let prop; for (prop in data) { this.fields[prop].setValue(data[prop], skipTrigger); } this.toggleGetNotified(); if (skipTrigger) return; this.triggerOnChange(); } triggerOnChange() { const callback = this.changeHandler; this.getNotified && callback && callback(this.raw); } triggerOnError() { const callback = this.errorHandler; this.getNotified && callback && callback(this.error); } get value() { const data = {}; let fieldName; for (fieldName in this.fields) { data[fieldName] = this.fields[fieldName].value; } return data; } get raw() { const data = {}; let fieldName; for (fieldName in this.fields) { data[fieldName] = this.fields[fieldName].raw; } return data; } getUpdates() { const data = {}; let fieldName; for (fieldName in this.fields) { if (this.fields[fieldName].isDirty()) { data[fieldName] = this.fields[fieldName].value; } } return data; } setError(errors, skipTrigger) { this.toggleGetNotified(); let prop; for (prop in errors) { this.fields[prop].setError(errors[prop], skipTrigger); } this.toggleGetNotified(); if (skipTrigger) return; this.triggerOnError(); } get error() { const errors = {}; let fieldName; for (fieldName in this.fields) { errors[fieldName] = this.fields[fieldName].error; } return errors; } isDirty() { let fieldName; for (fieldName in this.fields) { if (this.fields[fieldName].isDirty()) return true; } return false; } makePristine() { this.toggleGetNotified(); let fieldName; for (fieldName in this.fields) { this.fields[fieldName].makePristine(); } this.toggleGetNotified(); this.triggerOnError(); } reset() { this.toggleGetNotified(); let fieldName; for (fieldName in this.fields) { this.fields[fieldName].reset(); } this.toggleGetNotified(); this.triggerOnError(); this.triggerOnChange(); } _validate(skipAttachError) { let status = true; this.toggleGetNotified(); let fieldName; for (fieldName in this.fields) { let validity; if (skipAttachError) { validity = this.fields[fieldName].isValid(); } else { validity = this.fields[fieldName].validate(); } if (!validity && this.config.stopOnError) { status = false; break; } status = validity && status; } this.toggleGetNotified(); return status; } validate() { const validity = this._validate(false); this.triggerOnError(); return validity; } isValid() { return this._validate(true); } } function strDecoder(val) { if (typeof val !== "string") return ["", `Expected a string, got ${val}`]; if (val === "") { return ["", `This field is required`]; } return [val, ""]; } function numDecoder(val) { if (val === "") return [NaN, "This field is required"]; try { return [JSON.parse(val), ""]; } catch (e) { return [NaN, `Expected a number, got ${val}`]; } } function boolDecoder(val) { if (val === "") return [false, "This field is required"]; try { return [JSON.parse(val), ""]; } catch (e) { return [false, `Expected a number, got ${val}`]; } } function str(...validators) { return new Field(strDecoder, ...validators); } function num(...validators) { return new Field(numDecoder, ...validators); } function bool(...validators) { return new Field(boolDecoder, ...validators); } exports.Field = Field; exports.Form = Form; exports.bool = bool; exports.boolDecoder = boolDecoder; exports.defaultConfig = defaultConfig; exports.num = num; exports.numDecoder = numDecoder; exports.str = str; exports.strDecoder = strDecoder; Object.defineProperty(exports, '__esModule', { value: true }); }));