What I Want!
To Do:
Currently depends on:
MUST HAVE!!!
// tsconfig.json
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
The mission is to have a "single source of truth" for Form data. We do this using the reflect-metadata typescript library which allows us to add new decorators (@field, @editable) to a typescript class. The @editable decorator tells the parser to grab that field name. Then we use the field name to reflect the @field metadata with the FieldConfig being passed into the @field decorator.
The field setup and parsing is handled by the Form.ts class. It takes the model (containing the special decorators) and builds an array of fields based on the given FieldConfigs of the model. Using this pattern, along with class-validator, allows a beautiful Form handling experience.
The Form.fields array makes it easy to loop over and generate form fields dymanically. There are form options which specify when to validate input or clear errors (on input, change, focus, blur, etc.). It's also very easy to get data out of the Form by calling Form.model.
** Main things left to tackle:
// Business.ts
import { Length, IsEmail, IsString } from "class-validator";
import { editable, field } from "../typescript.utils";
import { FieldConfig } from "../FieldConfig";
class Business {
id: string;
@editable
@Length(10, 90)
@IsString()
@field(
new FieldConfig({
el: "input",
type: "text",
label: "Business Name",
required: true,
classname: "col-span-4 sm:col-span-2",
attributes: { placeholder: "Business Name" },
})
)
name: string = "";
@editable
@IsEmail()
@field(
new FieldConfig({
el: "input",
type: "email",
label: "Email Address",
required: true,
classname: "col-span-4 sm:col-span-2",
attributes: { placeholder: "Email Address" },
})
)
email: string = "";
@editable
@Length(10, 240)
@field(
new FieldConfig({
el: "textarea",
type: "text",
label: "Description",
required: true,
classname: "col-span-4 sm:col-span-2",
attributes: { placeholder: "Description" },
})
)
description: string = "";
avatar_url: string = "";
// Address
address_1: string = "";
address_2: string = "";
city: string = "";
state: string = "";
zip: string = "";
@editable
@IsString()
@field(
new FieldConfig({
el: "select",
type: "select",
label: "Business Status",
required: true,
classname: "col-span-4 sm:col-span-2",
ref_key: "business_statuses",
})
)
status;
}
The model (in the section) above can be attached by one of the following methods:
const form = new Form({ model: new Business() });
or;
const form = new Form();
form.model = new Busniess();
However, this ONLY sets the model.
MODEL AND FIELDS ARE DIFFERENT THINGS!
Call form.buildFields()
to build/set the form.fields with the model's field configurations.
If the model's fields already have data, the field data will be reflected in form.fields.value.
Validation!
form.validation_options
.TODO:
REFERENCE DATA:
Attach reference data to dropdowns by calling form.attachRefData(refData).
Note: attachRefData can only be called after fields are built. You gotta have fields to attach the data too.
Another Note: Reference data MUST BE in the format (for now):
{
"ref_key": [
{ "value": 0, "label": "First Choice" },
{ "value": 1, "label": "Second Choice" }
]
}
AND! Make sure to call form.destroy() to remove event listeners!
^^ I'm working on a way to do some of this automagically.
FORM.TS TODO:
Form.fields are of type FieldConfig.ts The constructor will attempt to parse the input type and add a sensable default to the field.value (type text defaults to "", type number defaults to 0, etc.).
Also contains the HTML Node which is being validated/targeted.
class FieldConfig {
constructor(init?: Partial<FieldConfig>) {
Object.assign(this, init);
this.attributes["type"] = this.type;
if (
this.type === "text" ||
this.type === "email" ||
this.type === "password" ||
this.type === "string"
) {
this.value.set("");
}
if (this.type === "number") {
this.value.set(0);
}
if (this.type === "decimal") {
this.value.set(0.0);
}
if (this.type === "boolean" || this.type === "choice") {
this.value.set(false);
}
if (this.el === "select" || this.el === "dropdown") {
this.options = [];
}
if (!this.attributes["title"]) {
this.attributes["title"] = this.label || this.name;
}
}
//! DO NOT SET NAME. IT'S SET AUTOMATICALLY BY FORM.TS!
name: string;
// Main use is to add and remove event listeners
node: HTMLElement;
el: string; // Element to render in your frontend
type: string = "text"; // Defaults to text, for now
label: string;
classname: string;
required: boolean = false;
value: Writable<any> = writable(null);
options?: any[];
ref_key?: string; // Reference data key
hint?: string; // Mainly for textarea, for now
group?: FieldGroup;
step?: FieldStep;
/**
* * String array of things like:
* -- type="text || email || password || whatever"
* -- class='input class'
* -- disabled
* -- title='input title'
* -- etc.
*/
attributes: object = {};
/**
* Validation Errors!
* We're mainly looking for the class-validator "constraints"
* One ValidationError object can have multiple errors (constraints)
*/
errors: Writable<ValidationError> = writable(null);
clearValue = () => {
this.value.set(null);
};
clearErrors = () => {
this.errors.set(null);
};
clear = () => {
this.clearValue();
this.clearErrors();
};
}
This is where the specialized (reflect-metadata) decorators are declared.
@editable and @field
This generates the form dynamically based on the @fields on the TS model. I would like to remove the tailwind parts for broader use, but it's good for testing right now.
Eventually the area with inputs will be a named <slot> to pass in something like a <Fields prop={field} \> type of component.
Just run it and see how you feel about this whole method.
Note that you will need to have Node.js 15.7.0 installed, for now.
Install the dependencies...
cd formvana
npm i
...then start Rollup:
npm run dev
Navigate to localhost:5000. You should see your app running.