Skip to content

Commit

Permalink
Initial commit moving project from bitbucket
Browse files Browse the repository at this point in the history
  • Loading branch information
hugo-chq committed Aug 12, 2020
1 parent fbf9978 commit 20f2a32
Show file tree
Hide file tree
Showing 14 changed files with 1,956 additions and 2 deletions.
534 changes: 532 additions & 2 deletions README.md

Large diffs are not rendered by default.

Binary file added example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added nativeandweb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "zustand-forms",
"version": "0.3.1",
"description": "**fast typesafe form states as zustand stores**",
"main": "./lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx",
"prepare": "npm run build"
},
"repository": {
"type": "git",
"url": "git+ssh://git@bitbucket.org/conduct/react-helpers-forms.git"
},
"keywords": ["form", "forms", "zustand", "react", "typescript", "validation"],
"author": "Hugo McPhee",
"license": "MIT",
"homepage": "https://bitbucket.org/conduct/react-helpers-forms#readme",
"typings": "./lib/index.d.ts",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.7.1",
"@typescript-eslint/parser": "^3.7.1",
"eslint": "^7.5.0",
"eslint-config-react-app": "^5.2.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-react": "^7.20.5",
"eslint-plugin-react-hooks": "^4.0.8",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"babel-eslint": "^10.1.0",
"prettier": "^2.0.5",
"typescript": "^3.9.7",
"react": "^16.13.1",
"@types/react": "^16.9.43"
},
"files": [
"lib/**/*"
],
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
},
"dependencies": {
"immer": "^7.0.7",
"zustand": "^2.2.3"
},
"peerDependencies": {
"react": "^16.13.1"
}
}
22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { StoreApi } from "zustand";

export { default as makeMakeFormStore } from "./makeFormStore";
export {
makeValidatorFunction as makeValidator,
makeValidatorUtils,
getTypedMakeValidator,
} from "./makeFormStore/validatorFunctionUtils";
export { makeFormHooks } from "./makeFormHooks";

// @ts-ignore: expects "export type {..." but that only works in the module, not if this is in a local folder
export { MakeFormStoresHelperTypes } from "./utils/typeHelpers";

export type InputIdFromFormApi<
T_FormApi extends StoreApi<any>
> = keyof ReturnType<T_FormApi["getState"]>["inputStates"];

// export type {
// InputValue,
// InputValueFromOptions,
// MakeInputsOptions,
// } from "./makeFormStore/types";
132 changes: 132 additions & 0 deletions src/makeFormHooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import useLatestDefined from "../utils/useLatestDefined";
import makeMakeFormStore from "../makeFormStore";
import { InputValueFromOptions } from "../makeFormStore/types";
import { MakeFormStoresHelperTypes } from "../utils/typeHelpers";
import shallow from "zustand/shallow";

type AllFormStoresObject = Record<
string,
{
hook: ReturnType<ReturnType<typeof makeMakeFormStore>>[0];
api: ReturnType<ReturnType<typeof makeMakeFormStore>>[1];
}
>;

export function makeFormHooks<T_AllFormStores extends AllFormStoresObject>(
allFormStores: T_AllFormStores
) {
type FormStores = typeof allFormStores;
// type FormName = keyof FormStores; // FormStoresHelperTypes["FormStores"] so it works with the helper types

type FormStoresHelperTypes = MakeFormStoresHelperTypes<typeof allFormStores>;
type FormName = FormStoresHelperTypes["FormName"];
type InputIdByFormName = FormStoresHelperTypes["InputIdByFormName"];

type AnyFormStoreHookUnion = {
[K_FormName in FormName]: FormStores[K_FormName]["hook"];
}[FormName];

type AnyFormStoreApiUnion = {
[K_FormName in FormName]: FormStores[K_FormName]["api"];
}[FormName];

type AnyFormStoreHook = UnionToIntersection<AnyFormStoreHookUnion>;
type AnyFormStoreApi = UnionToIntersection<AnyFormStoreApiUnion>;

// using FormStoresHelperTypes["InputIdByFormName"] so it works with the helper types
type FormStoreHookFromName<
T_FormName extends FormName
> = FormStores[T_FormName]["hook"];

type InputIdsFromFormName<
T_FormName extends keyof FormStores
> = keyof ReturnType<
FormStores[T_FormName]["api"]["getState"]
>["inputStates"];

type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends (k: infer I) => void
? I
: never;

function useFormInput<
T_FormName extends FormName,
T_InputId extends InputIdByFormName[T_FormName]
>({ formName, inputId }: { inputId: T_InputId; formName: T_FormName }) {
type T_FormStore = ReturnType<
typeof allFormStores[typeof formName]["api"]["getState"]
>;

const useFormStore = allFormStores[formName].hook;

const [inputState, updateInput, toggleFocus] = useFormStore(
(state: T_FormStore) => [
state.inputStates[inputId],
state.updateInput,
state.toggleFocus,
],
shallow
);

const {
isValid,
isValidServer,
hasBeenUnfocused,
localErrorTypes,
localErrorTextByErrorType,
serverErrorTexts,
isFocused,
isEdited,
timeUpdated,
value,
validatorsOptions,
} = inputState;

const hasHadServerErrors = serverErrorTexts.length > 0;
const canShowServerErrors = !isValidServer;
const canShowLocalErrors =
(hasBeenUnfocused && localErrorTypes.length > 0) || hasHadServerErrors;
const canShowErrors = canShowLocalErrors || canShowServerErrors;

const valueIsEmpty = (typeof value === "string" && value === "") || false;
const shouldShowPlaceholder = valueIsEmpty && !isFocused;

const inlineErrorTexts = [
...localErrorTypes.map(
(errorType: string) => localErrorTextByErrorType[errorType]
),
// adds server error texts after local errors
...(canShowServerErrors ? serverErrorTexts : []),
];

const latestVisibleInlineErrorTexts = useLatestDefined({
value: inlineErrorTexts,
isDefined: inlineErrorTexts.length > 0,
});

// TODO fix the as any types (T_InputId isn't working there)
return {
value: inputState.value,
onChange: (newValue: InputValueFromOptions<any, any, any, any, any>) =>
updateInput({ inputId, newValue } as any),
onFocus: () => toggleFocus({ inputId, isFocused: true } as any),
onBlur: () => toggleFocus({ inputId, isFocused: false } as any),
isFocused,
isEdited,
isValid,
canShowErrors,
hasVisibleErrors: canShowErrors && !isValid,
inlineErrorTexts,
latestVisibleInlineErrorTexts,
localErrorTypes,
localErrorTextByErrorType,
serverErrorTexts,
shouldShowPlaceholder,
validatorsOptions,
valueIsEmpty,
};
}

return { useFormInput };
}
18 changes: 18 additions & 0 deletions src/makeFormStore/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// only a partial InputState object for properties with basic types
export const defaultInputState = {
localErrorTypes: [] as string[],
localErrorTextByErrorType: {} as Record<string, string>,
serverErrorTexts: [] as string[],
// times
timeUpdated: 0,
timeBecameCheckable: 0, // whenever the input becomes visible
timeBecameUncheckable: 0, // whenever the input becomes hidden
timeFocused: 0,
timeUnfocused: 0,
// booleans
isValid: true,
isEdited: false,
hasBeenUnfocused: false,
isFocused: false,
isCheckable: true,
};
Loading

0 comments on commit 20f2a32

Please sign in to comment.