Skip to content

Commit

Permalink
Added Typescript support.
Browse files Browse the repository at this point in the history
  • Loading branch information
dtrelogan committed Feb 23, 2023
1 parent 71a5aed commit 84206ac
Show file tree
Hide file tree
Showing 22 changed files with 12,547 additions and 5,728 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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!
22 changes: 16 additions & 6 deletions __test__/reactComponents.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand All @@ -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 (
<FormScope formstate={fs} form={form}>
<Adaptor1 modelKey='address.line1'/>
<Adaptor1 name='address.line1'/>
<Adaptor2 modelKey='address.line2'/>
<Adaptor3 modelKey='thisGetsIgnored' name='address.line3'/>
</FormScope>
);
}
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}) {
Expand All @@ -459,7 +469,7 @@ describe('FormScope', () => {
form = { setFormstate: (f) => { fs = f(fs); }, adaptors: [Adaptor1, Adaptor2] };
return (
<FormScope formstate={fs} form={form}>
<Adaptor1 modelKey='address[line1]'/>
<Adaptor1 name='address[line1]'/>
<Adaptor2 modelKey='[address][line2]'/>
</FormScope>
);
Expand Down
308 changes: 308 additions & 0 deletions contract/api/index.d.ts
Original file line number Diff line number Diff line change
@@ -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: Model, form: Form<Model>): 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: <Model>(
initialModel: Model,
formValidationSchema?: FormValidationSchema<Model>
) => Formstate<Model>;

//
// Schema/Lookup
//

readonly getRootModelKey: <Model>(formstate: Formstate<Model>, id: number) => string;
readonly getModelKey: <Model>(formstate: Formstate<Model>, id: number) => string;
readonly getId: <Model>(formstate: Formstate<Model>, modelKey: string) => number;
readonly isScope: <Model>(formstate: Formstate<Model>, id: number) => boolean;
readonly isRequired: <Model>(formstate: Formstate<Model>, id: number, form: Form<Model>) => boolean;

//
// Form Status
//

// async

readonly isFormWaiting: <Model>(formstate: Formstate<Model>) => boolean;
readonly isFormAsyncError: <Model>(formstate: Formstate<Model>) => boolean;
readonly getFormAsyncErrorModelKeys: <Model>(formstate: Formstate<Model>) => readonly string[];

// submitting

readonly isInputDisabled: <Model>(formstate: Formstate<Model>) => boolean;
readonly isFormSubmitting: <Model>(formstate: Formstate<Model>) => boolean;
readonly isFormSubmittedAndUnchanged: <Model>(formstate: Formstate<Model>) => boolean;
readonly getFormSubmissionStartTime: <Model>(formstate: Formstate<Model>) => Date | undefined;
readonly getFormSubmissionEndTime: <Model>(formstate: Formstate<Model>) => Date | null | undefined;
readonly getFormSubmissionValidity: <Model>(formstate: Formstate<Model>) => boolean | null | undefined;
readonly getFormSubmissionAsyncErrorModelKeys: <Model>(formstate: Formstate<Model>) => readonly string[] | undefined;
readonly getFormSubmissionError: <Model>(formstate: Formstate<Model>) => Error | null | undefined;
readonly getFormSubmissionHistory: <Model>(formstate: Formstate<Model>) => readonly Formstate<Model>[] | undefined;
readonly wasSuccessfulSubmit: <Model>(formstate: Formstate<Model>) => boolean;
readonly setFormSubmitting: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly setFormSubmissionError: <Model>(formstate: Formstate<Model>, error: Error) => Formstate<Model>;
readonly setFormSubmitted: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly setInputDisabled: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly setInputEnabled: <Model>(formstate: Formstate<Model>) => Formstate<Model>;

// custom

readonly getFormCustomProperty: <Model>(formstate: Formstate<Model>, name: string) => unknown | undefined;
readonly setFormCustomProperty: <Model>(formstate: Formstate<Model>, name: string, value: unknown) => Formstate<Model>;

// validity

readonly isModelValid: <Model>(formstate: Formstate<Model>) => boolean;
readonly isModelInvalid: <Model>(formstate: Formstate<Model>) => boolean;
readonly isPrimedModelInvalid: <Model>(formstate: Formstate<Model>, calculatePrimed: CalculatePrimed<Model>) => 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: <Model>(formstate: Formstate<Model>, modelKey: string) => unknown;
readonly getInitialValue: <Model>(formstate: Formstate<Model>, modelKey: string) => unknown | undefined;
readonly setValueAndClearStatus: <Model>(formstate: Formstate<Model>, modelKey: string, value: unknown) => Formstate<Model>;

// validity

readonly isValid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isInvalid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isValidated: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isSynclyValid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isSynclyInvalid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isSynclyValidated: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly getMessage: <Model>(formstate: Formstate<Model>, modelKey: string) => string;
readonly setValid: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setInvalid: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setNotValidated: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setSynclyValid: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setSynclyInvalid: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setNotSynclyValidated: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setMessage: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;

// async

readonly isAsynclyValid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isAsynclyInvalid: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isAsynclyValidated: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isWaiting: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly getAsyncToken: <Model>(formstate: Formstate<Model>, modelKey: string) => AsyncToken | undefined;
readonly getAsyncStartTime: <Model>(formstate: Formstate<Model>, modelKey: string) => Date | undefined;
readonly getAsyncEndTime: <Model>(formstate: Formstate<Model>, modelKey: string) => Date | null | undefined;
readonly getAsyncError: <Model>(formstate: Formstate<Model>, modelKey: string) => Error | undefined;
readonly wasAsyncErrorDuringSubmit: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly setAsyncStarted: <Model>(formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setAsynclyValid: <Model>(asyncToken: AsyncToken, formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setAsynclyInvalid: <Model>(asyncToken: AsyncToken, formstate: Formstate<Model>, modelKey: string, message: string) => Formstate<Model>;
readonly setAsyncError: <Model>(asyncToken: AsyncToken, formstate: Formstate<Model>, modelKey: string, error: Error, message: string) => Formstate<Model>;

// touched

readonly isChanged: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isBlurred: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isSubmitting: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly isSubmitted: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly setChanged: <Model>(formstate: Formstate<Model>, modelKey: string) => Formstate<Model>;
readonly setBlurred: <Model>(formstate: Formstate<Model>, modelKey: string) => Formstate<Model>;
readonly setSubmitting: <Model>(formstate: Formstate<Model>, modelKey: string) => Formstate<Model>;
readonly setSubmitted: <Model>(formstate: Formstate<Model>, modelKey: string) => Formstate<Model>;

// custom

readonly getCustomProperty: <Model>(formstate: Formstate<Model>, modelKey: string, name: string) => unknown | undefined;
readonly setCustomProperty: <Model>(formstate: Formstate<Model>, modelKey: string, name: string, value: unknown) => Formstate<Model>;

// primed

readonly primeOnChange: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly primeOnBlur: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly primeOnChangeThenBlur: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;
readonly primeOnSubmit: <Model>(formstate: Formstate<Model>, modelKey: string) => boolean;

//
// Validation
//

readonly synclyValidate: <Model>(formstate: Formstate<Model>, modelKey: string, form: Form<Model>) => Formstate<Model>;
readonly synclyValidateForm: <Model>(formstate: Formstate<Model>, form: Form<Model>) => Formstate<Model>;
readonly validateForm: <Model>(formstate: Formstate<Model>, form: Form<Model>) => Formstate<Model>;
readonly asynclyValidate: <Model>(formstate: Formstate<Model>, modelKey: string, form: Form<Model>) => Formstate<Model>;
readonly asynclyValidateForm: <Model>(formstate: Formstate<Model>, form: Form<Model>) => Formstate<Model>;
readonly getPromises: <Model>(formstate: Formstate<Model>) => readonly Promise<unknown>[];
// There is a simple 'change' function that skips validation, called setValueAndClearStatus
readonly changeAndValidate: <Model>(formstate: Formstate<Model>, modelKey: string, value: unknown, form: Form<Model>) => Formstate<Model>;

//
// 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: <Model>(form: Form<Model>, value: unknown, id: number) => void;
readonly handleBlur: <Model>(form: Form<Model>, id: number) => void;

//
// Convenience Functions
//

readonly startFormSubmission: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly cancelFormSubmission: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly cancelFormSubmissionKeepInputDisabled: <Model>(formstate: Formstate<Model>) => Formstate<Model>;
readonly driveFormSubmission: <Model>(form: Form<Model>, submitValidModel: SubmitValidModel<Model>) => void;

//
// Dynamic Forms
//

readonly addModelKey: <Model, NewModel>(
formstate: Formstate<Model>,
modelKey: string,
initialModel: NewModel,
formValidationSchema: FormValidationSchema<NewModel>
) => Formstate<NewModel>;

readonly deleteModelKey: <Model, NewModel>(
formstate: Formstate<Model>,
modelKey: string,
) => Formstate<NewModel>;

readonly deleteModelKeyAndValidateParentScope: <Model, NewModel>(
formstate: Formstate<Model>,
modelKey: string,
form: Form<Model>
) => Formstate<NewModel>;

//
// React Component Adaptor
//

readonly bindToSetStateComponent: <Model>(
component: Component,
statePropertyName?: string
) => Form<Model>;
}

//
//
//
//
// 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<Model> // A nested form will RECEIVE these props from rff
{
readonly form: Form<Model>;
readonly formstate: Formstate<Model>;
}

export interface RffProps<Model> extends RffFormProps<Model> // An adaptor will RECEIVE these props from rff
{
readonly modelKey: string;
}

export function createRffAdaptor<Props, Model>(
Component: React.FunctionComponent<Props & RffProps<Model>>
): React.FunctionComponent<Props & FormFieldName>;

export function createRffNestedFormAdaptor<Props, Model>(
Component: React.FunctionComponent<Props & RffFormProps<Model>>
): React.FunctionComponent<Props & { nestedForm: true }>;



export interface ScopeOrFieldProps<Model>
{
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<Model, unknown>; //<Model, Model[name]>;
readonly validateAsync?: AsyncValidationFunction<Model, unknown>; //<Model, Model[name]>;
}

export type ValidatedRootScopeProps<Model> = RffFormProps<Model> & Omit<ScopeOrFieldProps<Model>, "name">;

export function FormScope<Model>(
props: PropsWithChildren<RffFormProps<Model> | ScopeOrFieldProps<Model> | ValidatedRootScopeProps<Model>>,
context?: any
): ReactElement<any, any>;

export function FormField<Model>(
props: PropsWithChildren<ScopeOrFieldProps<Model>>,
context?: any
): ReactElement<any, any>;

//
//
// useFormstate and rff
//
//

export function useFormstate<Model, CustomOptions extends Options<Model>>(
initialFormstate: Formstate<Model> | (() => Formstate<Model>),
options: CustomOptions
): [Formstate<Model>, Form<Model>];

export const rff: Rff;
Loading

0 comments on commit 84206ac

Please sign in to comment.