diff --git a/README.md b/README.md
index 66eeeb9..66cba8f 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ npm install --save react-formstate-fp
## Examples and documentation
- [Basic example](/doc/BasicExample.md)
+- [Typescript example](/doc/Typescript.md)
+- [How is this better than raw React?](/doc/Why.md)
- [Why the departure from react-formstate?](/doc/WhyTheFpBranch.md)
- [Initialization](/doc/Initialization.md)
- [Binding inputs to formstate](/doc/Binding.md)
@@ -47,5 +49,6 @@ Basically no dependencies other than React.
- Assumes an es5 environment (for example: Object.keys and Array.isArray).
- Assumes es6 promises. (This is the only polyfill requirement beyond es5.)
+- Works with Typescript.
Thanks for your interest!
diff --git a/__test__/reactComponents.spec.jsx b/__test__/reactComponents.spec.jsx
index a384e2d..f1acc2e 100644
--- a/__test__/reactComponents.spec.jsx
+++ b/__test__/reactComponents.spec.jsx
@@ -404,7 +404,7 @@ describe('FormScope', () => {
render(Test);
expect(called).toBe(true);
});
- test('adaptors can be passed modelKey', () => {
+ test('adaptors can be passed name (or modelKey)', () => {
let fs, form;
let called1 = false;
function Adaptor1({formstate, modelKey, form}) {
@@ -422,21 +422,31 @@ describe('FormScope', () => {
expect(typeof(form.setFormstate)).toBe('function');
return null;
}
+ let called3 = false;
+ function Adaptor3({formstate, modelKey, form}) {
+ called3 = true;
+ expect(formstate.nestedScopeId).toBe(null);
+ expect(modelKey).toBe('address.line3');
+ expect(typeof(form.setFormstate)).toBe('function');
+ return null;
+ }
function Test() {
- fs = rff.initializeFormstate({address: {line1: '', line2: ''}});
- form = { setFormstate: (f) => { fs = f(fs); }, adaptors: [Adaptor1, Adaptor2] };
+ fs = rff.initializeFormstate({address: {line1: '', line2: '', line3: ''}});
+ form = { setFormstate: (f) => { fs = f(fs); }, adaptors: [Adaptor1, Adaptor2, Adaptor3] };
return (
-
+
+
);
}
render(Test);
expect(called1).toBe(true);
expect(called2).toBe(true);
+ expect(called3).toBe(true);
});
- test('modelKeys passed to adaptors are normalized', () => {
+ test('names passed to adaptors are normalized', () => {
let fs, form;
let called1 = false;
function Adaptor1({formstate, modelKey, form}) {
@@ -459,7 +469,7 @@ describe('FormScope', () => {
form = { setFormstate: (f) => { fs = f(fs); }, adaptors: [Adaptor1, Adaptor2] };
return (
-
+
);
diff --git a/contract/api/index.d.ts b/contract/api/index.d.ts
new file mode 100644
index 0000000..c8b4670
--- /dev/null
+++ b/contract/api/index.d.ts
@@ -0,0 +1,308 @@
+import type { CalculatePrimed, Options, Form } from '../core/Form';
+import type { Formstate } from '../core/Formstate';
+import type { AsyncToken, AsyncValidationFunction, FormValidationSchema, ValidationFunction } from '../core/FormValidationSchema';
+import type { Component, PropsWithChildren, ReactElement } from 'react';
+
+//
+//
+// Supporting Types
+//
+//
+
+export { CalculatePrimed, Options, Form } from '../core/Form';
+
+export { Formstate } from '../core/Formstate';
+
+export {
+ AsyncToken, AsyncWhen, ValidationFunction, AsyncValidationFunction, FieldValidationSchema,
+ ScopeValidationSchema, FieldValidationSchemaMap, ScopeValidationSchemaMap, FormValidationSchema
+} from '../core/FormValidationSchema';
+
+export interface SubmitValidModel
+{
+ (model: Model, form: Form): void;
+}
+
+//
+//
+// The main API interface
+//
+// Bundled into an rff object (below) so that you don't have to import 25 functions into every module
+// where you use react-formstate-fp.
+//
+// The downside of doing this is code size, when downloading to web browsers. I think I got a bit
+// careless in terms of bundling nearly everything in here. Some of these functions can certainly
+// be extracted and moved elsewhere.
+//
+//
+
+export interface Rff
+{
+ readonly initializeFormstate: (
+ initialModel: Model,
+ formValidationSchema?: FormValidationSchema
+ ) => Formstate;
+
+ //
+ // Schema/Lookup
+ //
+
+ readonly getRootModelKey: (formstate: Formstate, id: number) => string;
+ readonly getModelKey: (formstate: Formstate, id: number) => string;
+ readonly getId: (formstate: Formstate, modelKey: string) => number;
+ readonly isScope: (formstate: Formstate, id: number) => boolean;
+ readonly isRequired: (formstate: Formstate, id: number, form: Form) => boolean;
+
+ //
+ // Form Status
+ //
+
+ // async
+
+ readonly isFormWaiting: (formstate: Formstate) => boolean;
+ readonly isFormAsyncError: (formstate: Formstate) => boolean;
+ readonly getFormAsyncErrorModelKeys: (formstate: Formstate) => readonly string[];
+
+ // submitting
+
+ readonly isInputDisabled: (formstate: Formstate) => boolean;
+ readonly isFormSubmitting: (formstate: Formstate) => boolean;
+ readonly isFormSubmittedAndUnchanged: (formstate: Formstate) => boolean;
+ readonly getFormSubmissionStartTime: (formstate: Formstate) => Date | undefined;
+ readonly getFormSubmissionEndTime: (formstate: Formstate) => Date | null | undefined;
+ readonly getFormSubmissionValidity: (formstate: Formstate) => boolean | null | undefined;
+ readonly getFormSubmissionAsyncErrorModelKeys: (formstate: Formstate) => readonly string[] | undefined;
+ readonly getFormSubmissionError: (formstate: Formstate) => Error | null | undefined;
+ readonly getFormSubmissionHistory: (formstate: Formstate) => readonly Formstate[] | undefined;
+ readonly wasSuccessfulSubmit: (formstate: Formstate) => boolean;
+ readonly setFormSubmitting: (formstate: Formstate) => Formstate;
+ readonly setFormSubmissionError: (formstate: Formstate, error: Error) => Formstate;
+ readonly setFormSubmitted: (formstate: Formstate) => Formstate;
+ readonly setInputDisabled: (formstate: Formstate) => Formstate;
+ readonly setInputEnabled: (formstate: Formstate) => Formstate;
+
+ // custom
+
+ readonly getFormCustomProperty: (formstate: Formstate, name: string) => unknown | undefined;
+ readonly setFormCustomProperty: (formstate: Formstate, name: string, value: unknown) => Formstate;
+
+ // validity
+
+ readonly isModelValid: (formstate: Formstate) => boolean;
+ readonly isModelInvalid: (formstate: Formstate) => boolean;
+ readonly isPrimedModelInvalid: (formstate: Formstate, calculatePrimed: CalculatePrimed) => boolean;
+
+ //
+ // Field/Scope Status
+ //
+
+ // value
+
+ // It's usually easier to use formstate.model.x rather than getValue(formstate, 'x');
+ // The one exception is when you are using a modelKey like getValue(formstate, 'address.line1');
+ //
+ readonly getValue: (formstate: Formstate, modelKey: string) => unknown;
+ readonly getInitialValue: (formstate: Formstate, modelKey: string) => unknown | undefined;
+ readonly setValueAndClearStatus: (formstate: Formstate, modelKey: string, value: unknown) => Formstate;
+
+ // validity
+
+ readonly isValid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isInvalid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isValidated: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isSynclyValid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isSynclyInvalid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isSynclyValidated: (formstate: Formstate, modelKey: string) => boolean;
+ readonly getMessage: (formstate: Formstate, modelKey: string) => string;
+ readonly setValid: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setInvalid: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setNotValidated: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setSynclyValid: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setSynclyInvalid: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setNotSynclyValidated: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setMessage: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+
+ // async
+
+ readonly isAsynclyValid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isAsynclyInvalid: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isAsynclyValidated: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isWaiting: (formstate: Formstate, modelKey: string) => boolean;
+ readonly getAsyncToken: (formstate: Formstate, modelKey: string) => AsyncToken | undefined;
+ readonly getAsyncStartTime: (formstate: Formstate, modelKey: string) => Date | undefined;
+ readonly getAsyncEndTime: (formstate: Formstate, modelKey: string) => Date | null | undefined;
+ readonly getAsyncError: (formstate: Formstate, modelKey: string) => Error | undefined;
+ readonly wasAsyncErrorDuringSubmit: (formstate: Formstate, modelKey: string) => boolean;
+ readonly setAsyncStarted: (formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setAsynclyValid: (asyncToken: AsyncToken, formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setAsynclyInvalid: (asyncToken: AsyncToken, formstate: Formstate, modelKey: string, message: string) => Formstate;
+ readonly setAsyncError: (asyncToken: AsyncToken, formstate: Formstate, modelKey: string, error: Error, message: string) => Formstate;
+
+ // touched
+
+ readonly isChanged: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isBlurred: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isSubmitting: (formstate: Formstate, modelKey: string) => boolean;
+ readonly isSubmitted: (formstate: Formstate, modelKey: string) => boolean;
+ readonly setChanged: (formstate: Formstate, modelKey: string) => Formstate;
+ readonly setBlurred: (formstate: Formstate, modelKey: string) => Formstate;
+ readonly setSubmitting: (formstate: Formstate, modelKey: string) => Formstate;
+ readonly setSubmitted: (formstate: Formstate, modelKey: string) => Formstate;
+
+ // custom
+
+ readonly getCustomProperty: (formstate: Formstate, modelKey: string, name: string) => unknown | undefined;
+ readonly setCustomProperty: (formstate: Formstate, modelKey: string, name: string, value: unknown) => Formstate;
+
+ // primed
+
+ readonly primeOnChange: (formstate: Formstate, modelKey: string) => boolean;
+ readonly primeOnBlur: (formstate: Formstate, modelKey: string) => boolean;
+ readonly primeOnChangeThenBlur: (formstate: Formstate, modelKey: string) => boolean;
+ readonly primeOnSubmit: (formstate: Formstate, modelKey: string) => boolean;
+
+ //
+ // Validation
+ //
+
+ readonly synclyValidate: (formstate: Formstate, modelKey: string, form: Form) => Formstate;
+ readonly synclyValidateForm: (formstate: Formstate, form: Form) => Formstate;
+ readonly validateForm: (formstate: Formstate, form: Form) => Formstate;
+ readonly asynclyValidate: (formstate: Formstate, modelKey: string, form: Form) => Formstate;
+ readonly asynclyValidateForm: (formstate: Formstate, form: Form) => Formstate;
+ readonly getPromises: (formstate: Formstate) => readonly Promise[];
+ // There is a simple 'change' function that skips validation, called setValueAndClearStatus
+ readonly changeAndValidate: (formstate: Formstate, modelKey: string, value: unknown, form: Form) => Formstate;
+
+ //
+ // Event Handlers
+ //
+
+ // handleChange and handleBlur take an id rather than a modelKey so that, in the case of a dynamic form,
+ // a defunct change handler won't bind to a form field that was removed from the form (and possibly
+ // replaced by a field with the same name).
+ // This is an extreme edge case and if it were a possibility in your form you'd have to be prepared to
+ // catch the resulting exception, but arguably this is better than some orphaned change handler
+ // unexpectedly updating your form's data.
+ // That said, yes, in 99.99% of use cases this is not something to be concerned about, so having to use
+ // an rff.getId() call to use the changeHandler is admittedly a little cumbersome.
+
+ readonly handleChange: (form: Form, value: unknown, id: number) => void;
+ readonly handleBlur: (form: Form, id: number) => void;
+
+ //
+ // Convenience Functions
+ //
+
+ readonly startFormSubmission: (formstate: Formstate) => Formstate;
+ readonly cancelFormSubmission: (formstate: Formstate) => Formstate;
+ readonly cancelFormSubmissionKeepInputDisabled: (formstate: Formstate) => Formstate;
+ readonly driveFormSubmission: (form: Form, submitValidModel: SubmitValidModel) => void;
+
+ //
+ // Dynamic Forms
+ //
+
+ readonly addModelKey: (
+ formstate: Formstate,
+ modelKey: string,
+ initialModel: NewModel,
+ formValidationSchema: FormValidationSchema
+ ) => Formstate;
+
+ readonly deleteModelKey: (
+ formstate: Formstate,
+ modelKey: string,
+ ) => Formstate;
+
+ readonly deleteModelKeyAndValidateParentScope: (
+ formstate: Formstate,
+ modelKey: string,
+ form: Form
+ ) => Formstate;
+
+ //
+ // React Component Adaptor
+ //
+
+ readonly bindToSetStateComponent: (
+ component: Component,
+ statePropertyName?: string
+ ) => Form;
+}
+
+//
+//
+//
+//
+// Top-Level API
+//
+//
+//
+//
+
+//
+//
+// FormScope, FormField, and adaptor-related props
+//
+//
+
+export interface FormFieldName // If not wrapped by FormField, provide a field name TO the adaptor.
+{
+ readonly name?: string;
+}
+
+export interface RffFormProps // A nested form will RECEIVE these props from rff
+{
+ readonly form: Form;
+ readonly formstate: Formstate;
+}
+
+export interface RffProps extends RffFormProps // An adaptor will RECEIVE these props from rff
+{
+ readonly modelKey: string;
+}
+
+export function createRffAdaptor(
+ Component: React.FunctionComponent>
+): React.FunctionComponent;
+
+export function createRffNestedFormAdaptor(
+ Component: React.FunctionComponent>
+): React.FunctionComponent;
+
+
+
+export interface ScopeOrFieldProps
+{
+ readonly name: string;
+ readonly required?: boolean | string;
+ // name is not known at compile time and typescript cannot handle dynamic types. using unknown instead.
+ readonly validate?: ValidationFunction; //;
+ readonly validateAsync?: AsyncValidationFunction; //;
+}
+
+export type ValidatedRootScopeProps = RffFormProps & Omit, "name">;
+
+export function FormScope(
+ props: PropsWithChildren | ScopeOrFieldProps | ValidatedRootScopeProps>,
+ context?: any
+): ReactElement