shelving
Version:
Toolkit for using data in JavaScript.
96 lines (95 loc) • 3.52 kB
JavaScript
import { RequiredError } from "../../error/RequiredError.js";
import { Schema } from "../../schema/Schema.js";
import { DataStore } from "../../store/DataStore.js";
import { DictionaryStore } from "../../store/DictionaryStore.js";
import { awaitDispose } from "../../util/dispose.js";
import { splitMessage } from "../../util/error.js";
import { getRandomKey } from "../../util/random.js";
/** Store the current value of a form. */
export class FormStore extends DataStore {
/** Unique ID for the form. */
id = getRandomKey();
/** Key used for mounting the form */
key;
/** Schema for the current form. */
schema;
/**
* Store named error messages for individual fields.
* - Throwing a string triggers changes in this.
* - Rows prefixed with `fieldName:` are shown on those specific fields.
* - See `splitMessages()` in `shelving` for more information.
*/
messages = new DictionaryStore();
/** Get the current valid value for this form (throws string for invalid values). */
get validated() {
return (this.value = this.schema.validate(this.value));
}
get reason() {
return super.reason;
}
set reason(reason) {
if (typeof reason === "string") {
this.messages.value = splitMessage(reason);
super.reason = undefined;
}
else {
super.reason = reason;
}
}
constructor(schema, partialData = {}, messages) {
super(partialData);
this.key = `${this.id}:${JSON.stringify(partialData)}`;
this.schema = schema;
if (messages)
this.messages.value = typeof messages === "string" ? splitMessage(messages) : messages;
}
/** Get a named schema for a field of this form. */
requireSchema(name) {
const schema = this.schema.props[name];
if (schema instanceof Schema)
return schema;
throw new RequiredError(`Schema "${name}" does not exist in form`, { received: name, caller: this.requireSchema });
}
/** Publish a value for a field of this form. */
publish(name, unsafeValue) {
this.abort();
// Clear main message and this named message.
this.messages.delete("");
this.messages.delete(name);
try {
const value = this.requireSchema(name).validate(unsafeValue);
this.set(name, value);
}
catch (thrown) {
if (typeof thrown === "string")
this.messages.set(name, thrown);
else
this.reason = thrown;
// Save the value _even if_ it didn't validate so it's persisted.
// Everything gets validated before submit.
this.set(name, unsafeValue);
}
}
/**
* Validate and submit the current values of the form.
*
* @param callback Optional callback that takes the current (validated) value of the form, processes it (possibly asynchronously) and returns any new values.
*/
submit(callback, ...args) {
try {
const value = this.validated;
if (callback)
return this.run(callback, value, ...args);
return true;
}
catch (thrown) {
this.reason = thrown;
return false;
}
}
// Implement `AsyncDisposable`
async [Symbol.asyncDispose]() {
await awaitDispose(this.messages, // Dispose of messages store.
super[Symbol.asyncDispose]());
}
}