powerform
Version:
A powerful form model.
341 lines (340 loc) • 9.45 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bool = exports.num = exports.str = exports.boolDecoder = exports.numDecoder = exports.strDecoder = exports.Form = exports.defaultConfig = exports.Field = void 0;
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;
}
}
exports.Field = Field;
exports.defaultConfig = {
multipleErrors: false,
stopOnError: false,
};
class Form {
constructor(fields, config = exports.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);
}
}
exports.Form = Form;
function strDecoder(val) {
if (typeof val !== "string")
return ["", `Expected a string, got ${val}`];
if (val === "") {
return ["", `This field is required`];
}
return [val, ""];
}
exports.strDecoder = strDecoder;
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}`];
}
}
exports.numDecoder = numDecoder;
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}`];
}
}
exports.boolDecoder = boolDecoder;
function str(...validators) {
return new Field(strDecoder, ...validators);
}
exports.str = str;
function num(...validators) {
return new Field(numDecoder, ...validators);
}
exports.num = num;
function bool(...validators) {
return new Field(boolDecoder, ...validators);
}
exports.bool = bool;