solid-primitives
solid-primitives copied to clipboard
createForm proposal
createForm()
proposal.
Requirements
- headless, allowing developer to use any UI component library for inputs
- allowing great control with opt-out option by using helpers to make DX simpler and as close to native browser support as possible e.g. by using directives and built in features
- opt-in advanced features :
- validation
- field dependencies and controlling required/visible fields based on current form values
- supporting array values e.g. list of skills
- controlled by a config coming from backend
- well typed
- SSR compatible
Proposed design
Simple
It uses directives.
const formAdapter = new MyFormAdapter(); // inherits BasicAdapter
const {
isLoading: [isLoading, setLoading],
isSubmitting: [isSubmitting, setSubmitting],
directives: {
form,
bind
}
} = createForm(formAdapter); // actual hook call
<form use:form>
<fieldset disabled={isLoading() || isSubmitting()}>
<input type="text" name={FormFieldsType.firstName} use:bind />
<input type="text" name={FormFieldsType.surname} use:bind />
<input type="reset" value="Reset" use:bind />
<input type="submit" value="Submit" use:bind>
</fieldset>
</form>
MyFormAdapter.ts
:
import {
FormDataType,
SchemaQueryType
} from './myFormAdapter.types';
import { JsonSchemaAdapter } from './jsonSchemaAdapter';
import exampleSchema from './exampleSchema.json';
export class MyFormAdapter extends BasicAdapter {
validateChangedValues( // opt-in
changedValues: FormDataType
): FormDataType {
console.log('validating', { changedValues, errors });
return changedValues;
}
async submitForm(formData: FormDataType): Promise<void> {
console.log('submit', formData);
}
}
Advanced
Very explicit, giving loads of control. It uses JSON schema under the hood which is translated to signals. It's assumed the initial values come from the schema default
values.
const formAdapter = new MyFormAdapter(); // inherits JsonSchemaAdapter
const {
loadSchema,
changeField,
changeArrayFieldRow,
submitForm,
resetForm,
addArrayFieldRow,
removeArrayFieldRow,
} = formAdapter.getObservedActions(); // it's a way to access action triggers from a particular adapter
const onFormChanged = (changedValues: FormDataType) => {
// just in case you want the data to be accessible out of the form
console.log({ changedValues });
};
const [schemaQuery, setSchemaQuery] = createSignal<SchemaQueryType>({
// ID or anything that can be used to retrieve JSON schema
id: null,
});
const {
isLoading: [isLoading, setLoading],
isSubmitting: [isSubmitting, setSubmitting],
schema: [schema, setSchema],
initialValues: [initialValues, setInitialValues],
visibleFields: [visibleFields, setVisibleFields],
requiredFields: [requiredFields, setRequiredFields],
fieldErrors: [fieldErrors, setFieldErrors],
formError: [formError, setFormError],
hasFormSubmissionSucceeded: [
hasFormSubmissionSucceeded,
setFormSubmissionSucceeded,
],
changedValues: [changedValues, setChangedValues],
fieldOptions: [fieldOptions, setFieldOptions],
} = createForm(formAdapter, onFormChanged); // actual hook call
createEffect(() => {
loadSchema(schemaQuery()); // if the query changes, loadSchema() and re-initiate the entire form
});
<>
<input
type="text"
value={changedValues().firstName}
onInput={(e) =>
changeField(FormFieldsType.firstName, e.currentTarget.value)
}
/>
{visibleFields().includes(FormFieldsType.surname) && (
<input
type="text"
value={changedValues().surname}
onInput={(e) =>
changeField(FormFieldsType.surname, e.currentTarget.value)
}
/>
)}
<button
onClick={() => {
submitForm(changedValues());
}}
>
submit
</button>
<button
onClick={() => {
resetForm(initialValues());
}}
>
reset
</button>
<button
onClick={() => {
addArrayFieldRow(changedValues().dataTypeMultiChoice);
}}
>
add array item
</button>
<button
onClick={() => {
removeArrayFieldRow(
changedValues().dataTypeMultiChoice,
changedValues().dataTypeMultiChoice.length - 1
);
}}
>
remove last array item
</button>
<button
onClick={() => {
changeArrayFieldRow(
changedValues().dataTypeMultiChoice,
changedValues().dataTypeMultiChoice.length - 1,
{
key: 'changeKey',
title: 'changed title',
dataType: 'number',
format: 'AS_PROVIDED',
presenter: 'STANDARD',
}
);
}}
>
change last array item
</button>
<pre>
{JSON.stringify(
{
isLoading: isLoading(),
isSubmitting: isSubmitting(),
changedValues: changedValues(),
fieldOptions: fieldOptions(),
visibleFields: visibleFields(),
rules: visibleFields().rules,
requiredFields: requiredFields(),
hasFormSubmissionSucceeded: hasFormSubmissionSucceeded(),
formError: formError(),
fieldErrors: fieldErrors(),
isSurnameVisible: visibleFields().includes(FormFieldsType.surname),
},
undefined,
2
)}
</pre>
</>
MyFormAdapter.ts
:
import {
FormFieldsType,
FormDataType,
SchemaQueryType,
SchemaType,
SchemaError,
FieldOptionsType,
INITIAL_SCHEMA_DATA,
INITIAL_FORM_DATA,
INITIAL_FIELD_OPTIONS,
} from './myFormAdapter.types';
import { JsonSchemaAdapter } from './jsonSchemaAdapter';
import exampleSchema from './exampleSchema.json';
export class MyFormAdapter extends JsonSchemaAdapter {
schemaQuery: SchemaQueryType = { id: null };
schema: SchemaType = INITIAL_SCHEMA_DATA;
formFields: FormFieldsType[] = [];
formData: FormDataType = INITIAL_FORM_DATA;
fieldOptions: FieldOptionsType = INITIAL_FIELD_OPTIONS;
async loadSchema(schemaQuery: SchemaQueryType): Promise<SchemaType> {
await new Promise((resolve) => setTimeout(() => resolve(true), 1000));
return exampleSchema as SchemaType; // normally you'd fetch it from backend
}
validateChangedValues(
changedValues: FormDataType,
errors: SchemaError[]
): FormDataType {
console.log('validating', { changedValues, errors });
return changedValues;
}
postTransformChangedValues(changedValues: FormDataType): FormDataType {
return changedValues;
}
async submitForm(formData: FormDataType): Promise<void> {
console.log('submit', formData);
}
}
Here is the playground I'll try to keep up-to-date as much as possible: https://stackblitz.com/edit/solidjs-templates-grmrsw?file=src%2FactualCode%2FmyForm.tsx