├── www ├── .env.development ├── src │ ├── layouts │ │ ├── HomeLayout │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── MainLayout │ │ │ ├── index.css │ │ │ └── index.tsx │ │ └── index.ts │ ├── routes │ │ └── index.ts │ ├── apis │ │ ├── formDataApi.ts │ │ ├── resetFormApi.ts │ │ ├── formHasChangesApi.ts │ │ ├── formIsFillingApi.ts │ │ ├── formIsResettingApi.ts │ │ ├── isFormInvalidApi.ts │ │ ├── touchFieldApi.ts │ │ ├── validateFormApi.ts │ │ ├── formIsValidatingApi.ts │ │ ├── getFieldValueApi.ts │ │ ├── getFormErrorsApi.ts │ │ ├── fieldHasErrorApi.ts │ │ ├── getFieldErrorApi.ts │ │ ├── isFieldInvalidApi.ts │ │ ├── addFieldsetApi.ts │ │ ├── getFieldDefaultValueApi.ts │ │ ├── isFieldValidatingApi.ts │ │ ├── removeFieldsetApi.ts │ │ ├── setFieldTriggersApi.ts │ │ ├── moveFieldsetApi.ts │ │ ├── fillFormApi.ts │ │ ├── setFieldValueApi.ts │ │ ├── useFormHandlerApi.ts │ │ ├── setFieldDefaultValueApi.ts │ │ ├── InputFieldApi.ts │ │ ├── validateFieldApi.ts │ │ ├── RadioGroupFieldApi.ts │ │ ├── CheckboxGroupFieldApi.ts │ │ ├── CheckboxFieldApi.ts │ │ └── ComponentFieldApi.ts │ ├── pages │ │ ├── index.ts │ │ ├── Api │ │ │ ├── ResetForm │ │ │ │ └── index.tsx │ │ │ ├── GetFieldDefaultValue │ │ │ │ └── index.tsx │ │ │ ├── MoveFieldset │ │ │ │ └── index.tsx │ │ │ ├── AddFieldset │ │ │ │ └── index.tsx │ │ │ ├── GetFieldError │ │ │ │ └── index.tsx │ │ │ ├── SetFieldTriggers │ │ │ │ └── index.tsx │ │ │ ├── FormIsFilling │ │ │ │ └── index.tsx │ │ │ ├── RemoveFieldset │ │ │ │ └── index.tsx │ │ │ ├── FormIsResetting │ │ │ │ └── index.tsx │ │ │ ├── GetFieldValue │ │ │ │ └── index.tsx │ │ │ ├── IsFieldInvalid │ │ │ │ └── index.tsx │ │ │ ├── FormIsValidating │ │ │ │ └── index.tsx │ │ │ ├── TouchField │ │ │ │ └── index.tsx │ │ │ ├── IsFormInvalid │ │ │ │ └── index.tsx │ │ │ ├── FieldHasError │ │ │ │ └── index.tsx │ │ │ ├── FormHasChanges │ │ │ │ └── index.tsx │ │ │ ├── IsFieldValidating │ │ │ │ └── index.tsx │ │ │ ├── GetFormErrors │ │ │ │ └── index.tsx │ │ │ ├── SetFieldDefaultValue │ │ │ │ └── index.tsx │ │ │ ├── ValidateForm │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ └── Docs │ │ │ ├── Setup │ │ │ ├── index.tsx │ │ │ └── SetupCmd │ │ │ │ └── index.tsx │ │ │ ├── Validations │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ ├── QuickStart │ │ │ └── index.tsx │ │ │ ├── Components │ │ │ └── index.tsx │ │ │ └── ValidatingRadios │ │ │ └── index.tsx │ ├── interfaces │ │ ├── Tree.ts │ │ ├── Flatten.ts │ │ ├── Size.ts │ │ ├── CodeTab.ts │ │ ├── Tab.ts │ │ ├── TreeMenuItem.ts │ │ ├── MenuItem.ts │ │ └── index.ts │ ├── components │ │ ├── Sidebar │ │ │ └── index.css │ │ ├── CodeTabs │ │ │ └── index.css │ │ ├── suid │ │ │ └── index.ts │ │ ├── Code │ │ │ └── index.css │ │ ├── Navbar │ │ │ └── index.css │ │ ├── Tabs │ │ │ └── index.css │ │ ├── index.ts │ │ ├── Redirect │ │ │ └── index.tsx │ │ ├── SidebarMenu │ │ │ └── index.css │ │ ├── SidebarProvider │ │ │ └── index.tsx │ │ ├── TextInput │ │ │ └── index.tsx │ │ ├── Implementation │ │ │ └── index.tsx │ │ └── Radio │ │ │ └── index.tsx │ ├── implementations │ │ ├── ValidateOnForm │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── ValidationDelayForm │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── PasswordConfirmForm │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── ProductsForm │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── ConditionalValidationForm │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ ├── index.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── RadiosForm │ │ │ └── index.ts │ │ ├── UserForm │ │ │ ├── index.ts │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── CheckboxForm │ │ │ └── index.ts │ │ ├── SuidUserForm │ │ │ ├── index.ts │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── CheckboxesForm │ │ │ └── index.ts │ │ ├── MultiStepForm │ │ │ ├── index.ts │ │ │ ├── zod │ │ │ │ ├── types.ts │ │ │ │ ├── context.ts │ │ │ │ ├── schema.ts │ │ │ │ └── Step1.tsx │ │ │ └── yup │ │ │ │ ├── context.ts │ │ │ │ ├── types.ts │ │ │ │ ├── schema.ts │ │ │ │ └── Step1.tsx │ │ ├── PersonForm │ │ │ ├── index.ts │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── RadiosCompForm │ │ │ └── index.ts │ │ ├── SelectCompForm │ │ │ └── index.ts │ │ ├── CheckboxCompForm │ │ │ └── index.ts │ │ ├── SingleSelectForm │ │ │ └── index.ts │ │ ├── FileInputCompForm │ │ │ └── index.ts │ │ ├── TextInputCompForm │ │ │ └── index.ts │ │ ├── CheckboxesCompForm │ │ │ └── index.ts │ │ ├── ReferralsForm │ │ │ ├── index.ts │ │ │ ├── yup │ │ │ │ ├── types.ts │ │ │ │ └── schema.ts │ │ │ └── zod │ │ │ │ └── schema.ts │ │ ├── SingleTextInputForm │ │ │ └── index.ts │ │ └── index.ts │ ├── code-snippets │ │ ├── setFieldDefaultValue1.ts │ │ ├── addFieldset1.ts │ │ ├── moveFieldset1.ts │ │ ├── removeFieldset1.ts │ │ ├── formIsResetting1.ts │ │ ├── formIsValidating1.ts │ │ ├── formIsFilling1.ts │ │ ├── schemaZod1.ts │ │ ├── setFieldValue1.tsx │ │ ├── ValidatingCheckbox2.tsx │ │ ├── isFieldValidating1.ts │ │ ├── schemaYup1.ts │ │ ├── isFormInvalid1.ts │ │ ├── useFormHandler1.ts │ │ ├── schemaZod2.ts │ │ ├── fillForm1.ts │ │ ├── getFormErrors1.ts │ │ ├── formHasChanges1.ts │ │ ├── resetForm1.ts │ │ ├── formHasChanges2.ts │ │ ├── schemaYup2.ts │ │ ├── touchField1.tsx │ │ ├── validateForm1.ts │ │ ├── fieldHasError1.ts │ │ ├── getFieldError1.ts │ │ ├── getFieldValue1.ts │ │ ├── validateField1.ts │ │ ├── formData1.ts │ │ ├── isFieldInvalid1.ts │ │ ├── useFormHandler3.ts │ │ ├── getFieldDefaultValue1.ts │ │ ├── setFieldTriggersApi1.ts │ │ ├── ValidatingTextInput1.tsx │ │ ├── useFormHandler2.ts │ │ ├── formData2.ts │ │ ├── ValidatingCheckbox1.tsx │ │ ├── ValidatingSelect1.tsx │ │ ├── ValidatingRadios1.tsx │ │ └── ValidatingCheckboxes1.tsx │ ├── constants │ │ ├── index.ts │ │ ├── urls.ts │ │ └── navbarMenus.tsx │ ├── utils │ │ ├── clone │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── getRaw │ │ │ └── index.ts │ │ ├── flattenTree │ │ │ ├── mocks.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ └── loadSnippets │ │ │ └── index.tsx │ ├── assets │ │ ├── images │ │ │ ├── npm-logo.png │ │ │ ├── favicon-16x16.png │ │ │ ├── solid-form-handler.png │ │ │ └── logo.svg │ │ └── fonts │ │ │ ├── gordita-bold.woff │ │ │ ├── gordita-regular.woff │ │ │ └── gordita-regular-italic.woff │ ├── App.tsx │ ├── index.tsx │ └── index.css ├── .env.production ├── .gitignore ├── .prettierrc ├── .babelrc ├── jest.config.ts ├── index.html ├── vite.config.ts ├── tsconfig.json └── scripts │ └── syncExampleComponents.js ├── .gitignore ├── .prettierrc ├── packages └── lib │ ├── src │ ├── hocs │ │ ├── index.ts │ │ └── withFieldProvider │ │ │ └── index.tsx │ ├── hooks │ │ └── index.ts │ ├── constants │ │ ├── errors.ts │ │ ├── strings.ts │ │ ├── index.ts │ │ ├── keys.ts │ │ └── regexps.ts │ ├── interfaces │ │ ├── CommonObject.ts │ │ ├── Flatten.ts │ │ ├── ErrorMap.ts │ │ ├── BrowserNativeObject.ts │ │ ├── TupleKeys.ts │ │ ├── IsTuple.ts │ │ ├── Primitive.ts │ │ ├── CommonEvent.ts │ │ ├── FieldPath.ts │ │ ├── FlattenPaths.ts │ │ ├── SetFieldDefaultValueOptions.ts │ │ ├── ValidateFieldBehavior.ts │ │ ├── ValidateOptions.ts │ │ ├── FormHandlerOptions.ts │ │ ├── CommonFieldProps.ts │ │ ├── FormHandler.ts │ │ ├── ValidateFieldOptions.ts │ │ ├── FormStateUpdateBehavior.ts │ │ ├── Paths.ts │ │ ├── FormState.ts │ │ ├── FieldProps.ts │ │ ├── SetFieldValueOptions.ts │ │ ├── ValidationSchema.ts │ │ ├── FieldState.ts │ │ ├── NestedPaths.ts │ │ ├── FieldStore.ts │ │ └── index.ts │ ├── adapters │ │ └── index.ts │ ├── index.ts │ ├── utils │ │ ├── isNumber │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── objectValueExists │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── isInteger │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── reorderArray │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── FormErrorsException │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── formatObjectPath │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── ValidationError │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── getFieldsPaths │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── buildFieldChildrenPath │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── get │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── buildFieldParentPath │ │ │ ├── index.tsx │ │ │ └── index.test.ts │ │ ├── isEmpty │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── clone │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── objectPaths │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── index.ts │ │ ├── buildFieldStatePaths │ │ │ └── index.ts │ │ ├── set │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── equals │ │ │ └── index.ts │ │ ├── flattenObject │ │ │ ├── index.ts │ │ │ └── index.test.ts │ │ ├── buildFieldStatePath │ │ │ └── index.test.ts │ │ └── createStore │ │ │ ├── index.test.ts │ │ │ └── index.ts │ └── components │ │ └── index.ts │ ├── .storybook │ ├── decorators │ │ ├── index.ts │ │ └── Bootstrap5Theme.tsx │ ├── preview-head.html │ ├── preview.ts │ └── main.ts │ ├── .gitignore │ ├── examples │ ├── implementations │ │ ├── ReferralsForm │ │ │ ├── types.ts │ │ │ ├── index.stories.tsx │ │ │ └── schema.ts │ │ ├── FormImpl │ │ │ ├── index.stories.tsx │ │ │ └── index.test.tsx │ │ ├── CheckboxImpl │ │ │ ├── index.stories.tsx │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── SuidFormImpl │ │ │ └── index.stories.tsx │ │ ├── NestedDeepFieldImpl │ │ │ └── index.stories.tsx │ │ ├── CheckboxesImpl │ │ │ └── index.stories.tsx │ │ ├── ComplexFormImpl │ │ │ └── index.stories.tsx │ │ ├── MultiSelectImpl │ │ │ └── index.stories.tsx │ │ ├── NestedFieldImpl │ │ │ ├── index.stories.tsx │ │ │ └── index.test.tsx │ │ ├── ValidateOnImpl │ │ │ └── index.stories.tsx │ │ ├── CheckboxCompForm │ │ │ └── index.stories.tsx │ │ ├── FieldsetsFormImpl │ │ │ ├── index.stories.tsx │ │ │ └── index.test.tsx │ │ ├── NestedArrFieldImpl │ │ │ └── index.stories.tsx │ │ ├── ValidateOnZodImpl │ │ │ └── index.stories.tsx │ │ ├── AsyncValidationImpl │ │ │ └── index.stories.tsx │ │ ├── VanillaCompFormImpl │ │ │ └── index.stories.tsx │ │ ├── FieldsetsFormStress1 │ │ │ └── index.stories.tsx │ │ ├── ValidateFileInputImpl │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── ComplexNestedFieldsImpl │ │ │ └── index.stories.tsx │ │ ├── DependantValidationImpl │ │ │ ├── index.stories.tsx │ │ │ └── index.test.tsx │ │ ├── NestedFieldsetsFormImpl │ │ │ └── index.stories.tsx │ │ ├── SortableFieldsetsFormImpl │ │ │ └── index.stories.tsx │ │ ├── TemperatureConversionImpl │ │ │ └── index.stories.tsx │ │ ├── SelectImpl │ │ │ ├── index.stories.tsx │ │ │ ├── schemas.ts │ │ │ └── index.tsx │ │ ├── FileInputImpl │ │ │ ├── index.stories.tsx │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── TextInputImpl │ │ │ ├── index.stories.tsx │ │ │ ├── schemas.ts │ │ │ └── index.tsx │ │ └── ConditionalFormImpl │ │ │ ├── index.stories.tsx │ │ │ └── schemas.ts │ └── components │ │ ├── index.ts │ │ ├── legacy │ │ ├── index.ts │ │ └── __tests__ │ │ │ ├── zod │ │ │ ├── TextInput │ │ │ │ └── mocks.ts │ │ │ ├── Radio │ │ │ │ └── mocks.ts │ │ │ ├── Checkbox │ │ │ │ └── mocks.ts │ │ │ ├── Select │ │ │ │ └── mocks.ts │ │ │ ├── Checkboxes │ │ │ │ └── mocks.ts │ │ │ └── Radios │ │ │ │ └── mocks.ts │ │ │ └── yup │ │ │ ├── TextInput │ │ │ └── mocks.ts │ │ │ ├── Radio │ │ │ └── mocks.ts │ │ │ ├── Checkbox │ │ │ └── mocks.ts │ │ │ ├── Select │ │ │ └── mocks.ts │ │ │ ├── Checkboxes │ │ │ └── mocks.ts │ │ │ └── Radios │ │ │ └── mocks.ts │ │ ├── suid │ │ └── index.ts │ │ ├── __tests__ │ │ ├── zod │ │ │ ├── TextInput │ │ │ │ └── mocks.ts │ │ │ ├── Checkbox │ │ │ │ └── mocks.ts │ │ │ ├── Select │ │ │ │ └── mocks.ts │ │ │ ├── Checkboxes │ │ │ │ └── mocks.ts │ │ │ └── Radios │ │ │ │ └── mocks.ts │ │ └── yup │ │ │ ├── TextInput │ │ │ └── mocks.ts │ │ │ ├── Checkbox │ │ │ └── mocks.ts │ │ │ ├── Select │ │ │ └── mocks.ts │ │ │ ├── Checkboxes │ │ │ └── mocks.ts │ │ │ └── Radios │ │ │ └── mocks.ts │ │ └── TextInput │ │ └── index.tsx │ ├── babel.config.json │ ├── jest.config.ts │ └── tsconfig.json ├── pnpm-workspace.yaml ├── .github └── workflows │ ├── actions │ ├── setup-pnpm │ │ └── action.yml │ ├── setup-node │ │ └── action.yml │ ├── lib-publish-to-npm │ │ └── action.yml │ ├── www-build-and-test │ │ └── action.yml │ └── lib-build-and-test │ │ └── action.yml │ ├── www.yml │ └── lib.yml └── package.json /www/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL= -------------------------------------------------------------------------------- /www/src/layouts/HomeLayout/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .DS_Store -------------------------------------------------------------------------------- /www/.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=solid-form-handler -------------------------------------------------------------------------------- /www/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mainRoutes'; 2 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .DS_Store 4 | /dist -------------------------------------------------------------------------------- /www/.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true, "printWidth": 80 } 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": true, "printWidth": 120, "tabWidth": 2 } 2 | -------------------------------------------------------------------------------- /packages/lib/src/hocs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withFieldProvider'; 2 | -------------------------------------------------------------------------------- /packages/lib/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useFormHandler'; 2 | -------------------------------------------------------------------------------- /www/src/apis/formDataApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function formData(): T; 3 | -------------------------------------------------------------------------------- /www/src/apis/resetFormApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function resetForm(): void; 3 | -------------------------------------------------------------------------------- /www/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Api'; 2 | export * from './Docs'; 3 | -------------------------------------------------------------------------------- /packages/lib/.storybook/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bootstrap5Theme'; 2 | -------------------------------------------------------------------------------- /www/src/interfaces/Tree.ts: -------------------------------------------------------------------------------- 1 | export type Tree = Array<{ children?: Tree } & T>; 2 | -------------------------------------------------------------------------------- /packages/lib/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/lib/src/constants/errors.ts: -------------------------------------------------------------------------------- 1 | export const VALIDATION_ERROR = 'ValidationError'; 2 | -------------------------------------------------------------------------------- /www/src/apis/formHasChangesApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function formHasChanges(): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/formIsFillingApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function formIsFilling(): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/formIsResettingApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function formIsResetting(): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/isFormInvalidApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function isFormInvalid(): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/touchFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function touchField(path: string): void; 3 | -------------------------------------------------------------------------------- /www/src/apis/validateFormApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function validateForm(): Promise; 3 | -------------------------------------------------------------------------------- /www/src/interfaces/Flatten.ts: -------------------------------------------------------------------------------- 1 | export type Flatten = T extends Array ? U : T; 2 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/CommonObject.ts: -------------------------------------------------------------------------------- 1 | export type CommonObject = { [x: string]: any }; 2 | -------------------------------------------------------------------------------- /www/src/apis/formIsValidatingApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function formIsValidating(): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/getFieldValueApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function getFieldValue(path?: string): any; 3 | -------------------------------------------------------------------------------- /www/src/apis/getFormErrorsApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function getFormErrors(): FormFieldError[]; 3 | -------------------------------------------------------------------------------- /www/src/interfaces/Size.ts: -------------------------------------------------------------------------------- 1 | export type Size = 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'fluid'; 2 | -------------------------------------------------------------------------------- /packages/lib/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './yupSchema'; 2 | export * from './zodSchema'; 3 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/Flatten.ts: -------------------------------------------------------------------------------- 1 | export type Flatten = T extends Array ? U : T; 2 | -------------------------------------------------------------------------------- /www/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", "solid"] 3 | } 4 | -------------------------------------------------------------------------------- /www/src/apis/fieldHasErrorApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function fieldHasError(path: string): boolean; 3 | -------------------------------------------------------------------------------- /www/src/apis/getFieldErrorApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function getFieldError(path: string): string; 3 | -------------------------------------------------------------------------------- /www/src/apis/isFieldInvalidApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function isFieldInvalid(path: string): boolean; 3 | -------------------------------------------------------------------------------- /www/src/components/Sidebar/index.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | background-color: var(--sidebar-bg); 3 | } 4 | -------------------------------------------------------------------------------- /packages/lib/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .DS_Store 5 | yarn-error.log 6 | README.md -------------------------------------------------------------------------------- /packages/lib/src/constants/strings.ts: -------------------------------------------------------------------------------- 1 | export const DATA_CONTAINS_ERRORS = 'Your data contains errors'; 2 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/ErrorMap.ts: -------------------------------------------------------------------------------- 1 | export type ErrorMap = Array<{ path: string; message: string }>; 2 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/BrowserNativeObject.ts: -------------------------------------------------------------------------------- 1 | export type BrowserNativeObject = Date | FileList | File; 2 | -------------------------------------------------------------------------------- /www/src/apis/addFieldsetApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function addFieldset(options?: { basePath?: string }): void; 3 | -------------------------------------------------------------------------------- /www/src/apis/getFieldDefaultValueApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function getFieldDefaultValue(path?: string): any; 3 | -------------------------------------------------------------------------------- /www/src/apis/isFieldValidatingApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function isFieldValidating(path: string): boolean; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ValidateOnForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { name: string; email: string }; 2 | -------------------------------------------------------------------------------- /www/src/layouts/MainLayout/index.css: -------------------------------------------------------------------------------- 1 | #root { 2 | display: grid; 3 | grid-template-rows: auto 1fr; 4 | } 5 | -------------------------------------------------------------------------------- /www/src/code-snippets/setFieldDefaultValue1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.setFieldDefaultValue('name', 'John'); 3 | -------------------------------------------------------------------------------- /www/src/implementations/ValidationDelayForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { name: string; email: string }; 2 | -------------------------------------------------------------------------------- /www/src/apis/removeFieldsetApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function removeFieldset(index: number, basePath?: string): void; 3 | -------------------------------------------------------------------------------- /www/src/apis/setFieldTriggersApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function setFieldTriggers(path?: string, paths?: string[]): void; 3 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/TupleKeys.ts: -------------------------------------------------------------------------------- 1 | export type TupleKeys = Exclude; 2 | -------------------------------------------------------------------------------- /www/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './navbarMenus'; 2 | export * from './sidebarMenus'; 3 | export * from './urls'; 4 | -------------------------------------------------------------------------------- /www/src/implementations/PasswordConfirmForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { password: string; passwordConfirm: string }; 2 | -------------------------------------------------------------------------------- /www/src/implementations/ProductsForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Product = { 2 | name: string; 3 | quantity: number; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DocsLayout'; 2 | export * from './HomeLayout'; 3 | export * from './MainLayout'; 4 | -------------------------------------------------------------------------------- /www/src/utils/clone/index.ts: -------------------------------------------------------------------------------- 1 | export const clone = (data: T): T => { 2 | return JSON.parse(JSON.stringify(data)); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/IsTuple.ts: -------------------------------------------------------------------------------- 1 | export type IsTuple = number extends T['length'] ? false : true; 2 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/Primitive.ts: -------------------------------------------------------------------------------- 1 | export type Primitive = string | number | boolean | null | undefined | symbol | bigint; 2 | -------------------------------------------------------------------------------- /www/src/assets/images/npm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/images/npm-logo.png -------------------------------------------------------------------------------- /www/src/interfaces/CodeTab.ts: -------------------------------------------------------------------------------- 1 | export type CodeTab = { 2 | name: string; 3 | code: string; 4 | language?: string; 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/components/CodeTabs/index.css: -------------------------------------------------------------------------------- 1 | .code-tabs .code-loading { 2 | margin: 0px !important; 3 | border: 0px !important; 4 | } 5 | -------------------------------------------------------------------------------- /www/src/interfaces/Tab.ts: -------------------------------------------------------------------------------- 1 | import { JSXElement } from 'solid-js'; 2 | 3 | export type Tab = { text: string; children?: JSXElement }; 4 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/CommonEvent.ts: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | export type CommonEvent = JSX.EventHandlerUnion; 3 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FieldPath.ts: -------------------------------------------------------------------------------- 1 | import { FlattenPaths } from '@interfaces'; 2 | 3 | export type FieldPath = FlattenPaths; 4 | -------------------------------------------------------------------------------- /www/src/assets/fonts/gordita-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/fonts/gordita-bold.woff -------------------------------------------------------------------------------- /www/src/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /www/src/implementations/ConditionalValidationForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { 2 | isAdult: boolean; 3 | email?: string; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/assets/fonts/gordita-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/fonts/gordita-regular.woff -------------------------------------------------------------------------------- /www/src/implementations/RadiosForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupRadiosForm } from './yup'; 2 | export { Form as ZodRadiosForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/UserForm/index.ts: -------------------------------------------------------------------------------- 1 | export { UserForm as YupUserForm } from './yup'; 2 | export { UserForm as ZodUserForm } from './zod'; 3 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FlattenPaths.ts: -------------------------------------------------------------------------------- 1 | import { Paths } from '@interfaces'; 2 | 3 | export type FlattenPaths = Exclude, ''>; 4 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/SetFieldDefaultValueOptions.ts: -------------------------------------------------------------------------------- 1 | export type SetFieldDefaultValueOptions = { 2 | mapValue?: (value: any) => any; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/ValidateFieldBehavior.ts: -------------------------------------------------------------------------------- 1 | export type ValidateFieldBehavior = { 2 | recursive?: boolean; 3 | fill?: boolean; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/assets/images/solid-form-handler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/images/solid-form-handler.png -------------------------------------------------------------------------------- /www/src/implementations/CheckboxForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupCheckboxForm } from './yup'; 2 | export { Form as ZodCheckboxForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/SuidUserForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupSuidUserForm } from './yup'; 2 | export { Form as ZodSuidUserForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clone'; 2 | export * from './flattenTree'; 3 | export * from './getRaw'; 4 | export * from './loadSnippets'; 5 | -------------------------------------------------------------------------------- /packages/lib/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export * from './keys'; 3 | export * from './regexps'; 4 | export * from './strings'; 5 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/ValidateOptions.ts: -------------------------------------------------------------------------------- 1 | export type ValidateOptions = { 2 | silentValidation?: boolean; 3 | throwException?: boolean; 4 | }; 5 | -------------------------------------------------------------------------------- /www/src/implementations/CheckboxesForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupCheckboxesForm } from './yup'; 2 | export { Form as ZodCheckboxesForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupMultiStepForm } from './yup'; 2 | export { Form as ZodMultiStepForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/PersonForm/index.ts: -------------------------------------------------------------------------------- 1 | export { PersonForm as YupPersonForm } from './yup'; 2 | export { PersonForm as ZodPersonForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/RadiosCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupRadiosCompForm } from './yup'; 2 | export { Form as ZodRadiosCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/SelectCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupSelectCompForm } from './yup'; 2 | export { Form as ZodSelectCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ValidateOnForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupValidateOnForm } from './yup'; 2 | export { Form as ZodValidateOnForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/apis/moveFieldsetApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function moveFieldset( 3 | oldIndex?: number, 4 | newIndex?: number, 5 | basePath?: string 6 | ): void; 7 | -------------------------------------------------------------------------------- /www/src/assets/fonts/gordita-regular-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauriver21/solid-form-handler/HEAD/www/src/assets/fonts/gordita-regular-italic.woff -------------------------------------------------------------------------------- /www/src/implementations/CheckboxCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupCheckboxCompForm } from './yup'; 2 | export { Form as ZodCheckboxCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/SingleSelectForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupSingleSelectForm } from './yup'; 2 | export { Form as ZodSingleSelectForm } from './zod'; 3 | -------------------------------------------------------------------------------- /packages/lib/src/constants/keys.ts: -------------------------------------------------------------------------------- 1 | export const ROOT_KEY = '__ROOT__'; 2 | export const STATE_KEY = '__STATE__'; 3 | export const CHILDREN_KEY = '__CHILDREN__'; 4 | -------------------------------------------------------------------------------- /www/src/apis/fillFormApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function fillForm( 3 | data: T, 4 | options?: { 5 | silentValidation?: boolean; 6 | } 7 | ): Promise; 8 | -------------------------------------------------------------------------------- /www/src/implementations/FileInputCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupFileInputCompForm } from './yup'; 2 | export { Form as ZodFileInputCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ProductsForm/index.ts: -------------------------------------------------------------------------------- 1 | export { ProductsForm as YupProductsForm } from './yup'; 2 | export { ProductsForm as ZodProductsForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/TextInputCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupTextInputCompForm } from './yup'; 2 | export { Form as ZodTextInputCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # executable/launchable applications 3 | - 'www' 4 | # all packages in subdirs of packages/ and components/ 5 | - 'packages/*' 6 | -------------------------------------------------------------------------------- /www/src/code-snippets/addFieldset1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.addFieldset(); 3 | 4 | //For nested fieldset 5 | formHandler.addFieldset({ basePath: 'nested' }); 6 | -------------------------------------------------------------------------------- /www/src/code-snippets/moveFieldset1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.moveFieldset(0, 2); 3 | 4 | //For nested fieldset 5 | formHandler.removeFieldset(0, 2, 'nested'); 6 | -------------------------------------------------------------------------------- /www/src/code-snippets/removeFieldset1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.removeFieldset(1); 3 | 4 | //For nested fieldset 5 | formHandler.removeFieldset(2, 'nested'); 6 | -------------------------------------------------------------------------------- /www/src/implementations/CheckboxesCompForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupCheckboxesCompForm } from './yup'; 2 | export { Form as ZodCheckboxesCompForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/zod/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { schema } from './schema'; 3 | 4 | export type Schema = z.infer; 5 | -------------------------------------------------------------------------------- /www/src/implementations/PasswordConfirmForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupPasswordConfirmForm } from './yup'; 2 | export { Form as ZodPasswordConfirmForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ReferralsForm/index.ts: -------------------------------------------------------------------------------- 1 | export { ReferralsForm as YupReferralsForm } from './yup'; 2 | export { ReferralsForm as ZodReferralsForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/SingleTextInputForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupSingleTextInputForm } from './yup'; 2 | export { Form as ZodSingleTextInputForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ValidationDelayForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupValidationDelayForm } from './yup'; 2 | export { Form as ZodValidationDelayForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/apis/setFieldValueApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function setFieldValue( 3 | path: string, 4 | value: any, 5 | options?: SetFieldValueOptions 6 | ): Promise; 7 | -------------------------------------------------------------------------------- /www/src/code-snippets/formIsResetting1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.resetForm(); 3 | 4 | createEffect(() => { 5 | console.log(formHandler.formIsResetting()); 6 | }); 7 | -------------------------------------------------------------------------------- /www/src/interfaces/TreeMenuItem.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '@interfaces'; 2 | 3 | export type TreeMenuItem = T & { 4 | children?: TreeMenuItem[]; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './interfaces'; 3 | export { Field } from './components'; 4 | export { FormErrorsException } from './utils'; 5 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FormHandlerOptions.ts: -------------------------------------------------------------------------------- 1 | export type FormHandlerOptions = { 2 | silentValidation?: boolean; 3 | validateOn?: string[]; 4 | delay?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/apis/useFormHandlerApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function useFormHandler( 3 | validationSchema: ValidationSchema, 4 | options?: FormHandlerOptions 5 | ): FormHandler; 6 | -------------------------------------------------------------------------------- /www/src/code-snippets/formIsValidating1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.validateForm(); 3 | 4 | createEffect(() => { 5 | console.log(formHandler.formIsValidating()); 6 | }); 7 | -------------------------------------------------------------------------------- /www/src/implementations/ConditionalValidationForm/index.ts: -------------------------------------------------------------------------------- 1 | export { Form as YupConditionalValidationForm } from './yup'; 2 | export { Form as ZodConditionalValidationForm } from './zod'; 3 | -------------------------------------------------------------------------------- /www/src/implementations/ReferralsForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Referrals = { 2 | hostName: string; 3 | hostEmail: string; 4 | referrals: Array<{ name: string; email: string }>; 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/code-snippets/formIsFilling1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.fillForm({ name: 'John', age: 28 }); 3 | 4 | createEffect(() => { 5 | console.log(formHandler.formIsFilling()); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ReferralsForm/types.ts: -------------------------------------------------------------------------------- 1 | export type Referrals = { 2 | hostName: string; 3 | hostEmail: string; 4 | referrals: Array<{ name: string; email: string }>; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isNumber/index.ts: -------------------------------------------------------------------------------- 1 | export const isNumber = (input: any) => { 2 | if (input === '' || input === null) { 3 | return false; 4 | } 5 | 6 | return !isNaN(input); 7 | }; 8 | -------------------------------------------------------------------------------- /www/src/code-snippets/schemaZod1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | const userSchema = z.object({ 3 | name: z.string().min(1, 'name is required'), 4 | age: z.coerce.number().min(1, 'age is required'), 5 | }); 6 | -------------------------------------------------------------------------------- /www/src/implementations/ValidateOnForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | name: z.string().min(1, 'Required field'), 5 | email: z.string().email(), 6 | }); 7 | -------------------------------------------------------------------------------- /www/src/apis/setFieldDefaultValueApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function setFieldDefaultValue( 3 | path: string, 4 | defaultValue: any, 5 | options?: { 6 | mapValue?: (value: any) => any; 7 | } 8 | ): void; 9 | -------------------------------------------------------------------------------- /packages/lib/src/utils/objectValueExists/index.ts: -------------------------------------------------------------------------------- 1 | import { get } from '@utils'; 2 | 3 | export const objectValueExists = (data: any, path: string) => { 4 | return get(data, path) === undefined ? false : true; 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/components/suid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './Radio'; 4 | export * from './Radios'; 5 | export * from './Select'; 6 | export * from './TextInput'; 7 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isInteger/index.ts: -------------------------------------------------------------------------------- 1 | import { IS_INTEGER_REGEXP } from '@constants'; 2 | 3 | export const isInteger = (input: any = '') => { 4 | return Boolean(String(input)?.match(IS_INTEGER_REGEXP)?.length); 5 | }; 6 | -------------------------------------------------------------------------------- /www/src/implementations/PersonForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Person = { 2 | name: string; 3 | age: number; 4 | contact: { 5 | email: string; 6 | phone: string; 7 | address: string; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/lib/examples/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './FileInput'; 4 | export * from './Radios'; 5 | export * from './Select'; 6 | export * from './TextInput'; 7 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './Radio'; 4 | export * from './Radios'; 5 | export * from './Select'; 6 | export * from './TextInput'; 7 | -------------------------------------------------------------------------------- /packages/lib/examples/components/suid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './Radio'; 4 | export * from './Radios'; 5 | export * from './Select'; 6 | export * from './TextInput'; 7 | -------------------------------------------------------------------------------- /www/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { useRoutes } from '@solidjs/router'; 3 | import { mainRoutes } from '@routes'; 4 | 5 | export const App: Component = () => { 6 | return useRoutes(mainRoutes); 7 | }; 8 | -------------------------------------------------------------------------------- /www/src/interfaces/MenuItem.ts: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export type MenuItem = { 4 | text: string; 5 | route?: string; 6 | section?: boolean; 7 | icon?: JSX.Element; 8 | external?: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /www/src/implementations/UserForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | name: string; 3 | email: string; 4 | country: number; 5 | favoriteFoods: number[]; 6 | gender: 'male' | 'female' | 'other'; 7 | subscribed: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /www/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CodeTab'; 2 | export * from './Flatten'; 3 | export * from './MenuItem'; 4 | export * from './Size'; 5 | export * from './Tab'; 6 | export * from './Tree'; 7 | export * from './TreeMenuItem'; 8 | -------------------------------------------------------------------------------- /www/src/code-snippets/setFieldValue1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | 6 | formHandler.setFieldValue(name, value) 7 | } 8 | >; 9 | -------------------------------------------------------------------------------- /www/src/implementations/SuidUserForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | name: string; 3 | email: string; 4 | country: number; 5 | favoriteFoods: number[]; 6 | gender: 'male' | 'female' | 'other'; 7 | subscribed: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /www/src/apis/InputFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type InputFieldProps = CommonFieldProps & { 3 | mode: 'input'; 4 | onInput?: CommonEvent; 5 | onInputOptions?: SetFieldValueOptions; 6 | render: (field: InputFieldStore) => JSXElement; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/zod/TextInput/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | name: z.string().min(1, 'name is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | -------------------------------------------------------------------------------- /packages/lib/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CheckboxField'; 2 | export * from './CheckboxGroupField'; 3 | export * from './Field'; 4 | export * from './FileInputField'; 5 | export * from './InputField'; 6 | export * from './RadioGroupField'; 7 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingCheckbox2.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | (...)} 9 | />; 10 | -------------------------------------------------------------------------------- /www/src/code-snippets/isFieldValidating1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.validateForm(); 3 | 4 | createEffect(() => { 5 | console.log( 6 | formHandler.isFieldValidating('name'), 7 | formHandler.isFieldValidating('age') 8 | ); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/TextInput/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | name: z.string().min(1, 'name is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | -------------------------------------------------------------------------------- /www/src/code-snippets/schemaYup1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type User = { 3 | name: string; 4 | age: string; 5 | }; 6 | 7 | const userSchema: yup.Schema = yup.object({ 8 | name: yup.string().required(), 9 | age: yup.number().required(), 10 | }); 11 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/CommonFieldProps.ts: -------------------------------------------------------------------------------- 1 | import { CommonEvent, FieldProps, ValidateFieldOptions } from '@interfaces'; 2 | 3 | export interface CommonFieldProps extends FieldProps { 4 | onBlur?: CommonEvent; 5 | onBlurOptions?: ValidateFieldOptions; 6 | } 7 | -------------------------------------------------------------------------------- /www/src/apis/validateFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function validateField( 3 | path: string, 4 | options?: { 5 | silentValidation?: boolean; 6 | validateOn?: string[]; 7 | force?: boolean; 8 | delay?: number; 9 | } 10 | ): Promise; 11 | -------------------------------------------------------------------------------- /www/src/code-snippets/isFormInvalid1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | console.log(formHandler.isFormInvalid()); //true 3 | 4 | //Form filled with the expected schema data 5 | formHandler.fillForm({ name: 'John', age: 28 }); 6 | console.log(formHandler.isFormInvalid()); //false 7 | -------------------------------------------------------------------------------- /www/src/code-snippets/useFormHandler1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | const formHandler = useFormHandler(__VALIDATOR__Schema(userSchema)); 6 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FormHandler.ts: -------------------------------------------------------------------------------- 1 | import { useFormHandler } from '@hooks'; 2 | 3 | export type FormHandler = Omit, 'fillForm' | 'formData'> & { 4 | fillForm: (data: T) => Promise; 5 | formData: () => T; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/ValidateFieldOptions.ts: -------------------------------------------------------------------------------- 1 | import { ValidateOptions } from '@interfaces'; 2 | 3 | export type ValidateFieldOptions = ValidateOptions & { 4 | force?: boolean; 5 | delay?: number; 6 | validateOn?: string[]; 7 | omitTriggers?: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/lib/src/utils/reorderArray/index.ts: -------------------------------------------------------------------------------- 1 | export const reorderArray = (array: any[], oldIndex: number, newIndex: number) => { 2 | const items = [...array]; 3 | const [item] = items.splice(oldIndex, 1); 4 | items.splice(newIndex, 0, item); 5 | return items; 6 | }; 7 | -------------------------------------------------------------------------------- /www/src/apis/RadioGroupFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type RadioGroupFieldProps = CommonFieldProps & { 3 | mode: 'radio-group'; 4 | onChange?: CommonEvent; 5 | onChangeOptions?: SetFieldValueOptions; 6 | render: (field: RadioGroupFieldStore) => JSXElement; 7 | }; 8 | -------------------------------------------------------------------------------- /www/src/code-snippets/schemaZod2.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | const companySchema = z.object({ 3 | name: z.string().min(1, 'name is required'), 4 | contact: z.object({ 5 | email: z.string().email(), 6 | phone: z.string().min(1, 'name is required'), 7 | }), 8 | }); 9 | -------------------------------------------------------------------------------- /www/src/implementations/ProductsForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const productSchema = z.array( 4 | z.object({ 5 | name: z.string().min(1, 'Required field'), 6 | quantity: z.coerce.number().min(1, 'Quantity is required'), 7 | }) 8 | ); 9 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/yup/TextInput/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | name: string; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | name: yup.string().required(), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FormStateUpdateBehavior.ts: -------------------------------------------------------------------------------- 1 | import { ValidateFieldBehavior } from '@interfaces'; 2 | 3 | export type FormStateUpdateBehavior = { 4 | updateParent?: boolean; 5 | updateChild?: boolean; 6 | validateFieldBehavior?: ValidateFieldBehavior; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/lib/src/utils/FormErrorsException/index.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMap } from '@interfaces'; 2 | 3 | export class FormErrorsException { 4 | validationResult: ErrorMap; 5 | 6 | constructor(errors: ErrorMap) { 7 | this.validationResult = errors; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/actions/setup-pnpm/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup pnpm 2 | description: Setups pnpm to specified version. 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Setup pnpm 7 | uses: pnpm/action-setup@v2 8 | with: 9 | version: '7.27.0' 10 | -------------------------------------------------------------------------------- /packages/lib/.storybook/decorators/Bootstrap5Theme.tsx: -------------------------------------------------------------------------------- 1 | import { Component, JSXElement } from 'solid-js'; 2 | import 'bootstrap/dist/css/bootstrap.min.css'; 3 | 4 | export const Bootstrap5Theme: Component<{ children: JSXElement }> = (props) => { 5 | return props.children; 6 | }; 7 | -------------------------------------------------------------------------------- /www/src/apis/CheckboxGroupFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type CheckboxGroupFieldProps = CommonFieldProps & { 3 | mode: 'checkbox-group'; 4 | onChange?: CommonEvent; 5 | onChangeOptions?: SetFieldValueOptions; 6 | render: (field: CheckboxGroupFieldStore) => JSXElement; 7 | }; 8 | -------------------------------------------------------------------------------- /www/src/code-snippets/fillForm1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | const formHandler = useFormHandler(__VALIDATOR__Schema(userSchema)); 3 | formHandler.fillForm({ name: 'John', age: 28 }); 4 | console.log(formHandler.formData()); 5 | /** 6 | * -- Output: -- 7 | * {name: 'John', age: 28} 8 | */ 9 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/TextInput/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | name: string; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | name: yup.string().required(), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/Paths.ts: -------------------------------------------------------------------------------- 1 | import { BrowserNativeObject, NestedPaths, Primitive } from '@interfaces'; 2 | 3 | export type Paths = T extends Primitive | BrowserNativeObject 4 | ? K 5 | : K | `${K extends '' ? '' : `${K}.`}${NestedPaths}`; 6 | -------------------------------------------------------------------------------- /packages/lib/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", "solid"], 3 | "plugins": ["@babel/plugin-transform-runtime"], 4 | "env": { 5 | "test": { 6 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FormState.ts: -------------------------------------------------------------------------------- 1 | import { CHILDREN_KEY, STATE_KEY } from '@constants'; 2 | import { FieldState } from '@interfaces'; 3 | 4 | export type FormState = { 5 | [x: string]: { 6 | [STATE_KEY]: FieldState; 7 | [CHILDREN_KEY]: FormState | Array; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /www/src/implementations/ValidateOnForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Schema } from './types'; 3 | 4 | export const schema: yup.Schema = yup.object({ 5 | name: yup.string().required('Required field'), 6 | email: yup.string().email().required('Required field'), 7 | }); 8 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/Radio/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | gender: z.enum(['male', 'female', 'other'], { errorMap: () => ({ message: 'gender is a required field' }) }), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FieldProps.ts: -------------------------------------------------------------------------------- 1 | import { FormHandler } from '@interfaces'; 2 | 3 | export type FieldProps = { 4 | id?: string; 5 | error?: boolean; 6 | errorMessage?: string; 7 | formHandler?: FormHandler; 8 | name?: string; 9 | triggers?: string[]; 10 | value?: any; 11 | }; 12 | -------------------------------------------------------------------------------- /www/src/layouts/HomeLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Outlet } from '@solidjs/router'; 3 | import './index.css'; 4 | 5 | export const HomeLayout: Component = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /www/src/apis/CheckboxFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type CheckboxFieldProps = CommonFieldProps & { 3 | mode: 'checkbox'; 4 | onChange?: CommonEvent; 5 | onChangeOptions?: SetFieldValueOptions; 6 | checked?: boolean; 7 | uncheckedValue?: any; 8 | render: (field: CheckboxFieldStore) => JSXElement; 9 | }; 10 | -------------------------------------------------------------------------------- /www/src/code-snippets/getFormErrors1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | await formHandler.validateForm(); 3 | console.log(formHandler.getFormErrors()); 4 | /** 5 | * -- Output: -- 6 | * [ 7 | * { path: 'name', errorMessage: 'name is required' }, 8 | * { path: 'age', errorMessage: 'age is required' } 9 | * ] 10 | */ 11 | -------------------------------------------------------------------------------- /www/src/constants/urls.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = import.meta.env.BASE_URL; 2 | export const SOLID_JS_URL = 'https://www.solidjs.com'; 3 | export const SOLID_JS_STORES_URL = `${SOLID_JS_URL}/docs/latest/api#stores`; 4 | export const YUP_URL = 'https://github.com/jquense/yup'; 5 | export const SUID_URL = 'https://suid.io/'; 6 | -------------------------------------------------------------------------------- /.github/workflows/actions/setup-node/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup node 2 | description: Setups node to specified version. 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Setup Node 7 | uses: actions/setup-node@v3 8 | with: 9 | node-version: '16.x' 10 | registry-url: 'https://registry.npmjs.org' 11 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { FormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: FormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const Form = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/utils/formatObjectPath/index.ts: -------------------------------------------------------------------------------- 1 | export const formatObjectPath = (string: string) => { 2 | return string 3 | .replace(/\[/g, '.') 4 | .replace(/\]/g, '.') 5 | .replace(/\.\"/g, '.') 6 | .replace(/\"\./g, '.') 7 | .replace(/\.\./g, '.') 8 | .replace(/^\./, '') 9 | .replace(/\.$/, ''); 10 | }; 11 | -------------------------------------------------------------------------------- /www/src/utils/getRaw/index.ts: -------------------------------------------------------------------------------- 1 | import { snippetsStore } from '@utils'; 2 | 3 | export const getRaw = (path: string) => { 4 | path = path.replace(/^\//, ''); 5 | 6 | for (let key in snippetsStore.snippets) { 7 | if (key.match(`/${path}`)) { 8 | return snippetsStore.snippets[key]; 9 | } 10 | } 11 | 12 | return ''; 13 | }; 14 | -------------------------------------------------------------------------------- /www/src/implementations/ConditionalValidationForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Schema } from './types'; 3 | 4 | export const schema: yup.Schema = yup.object({ 5 | isAdult: yup.boolean().required(), 6 | email: yup 7 | .string() 8 | .when('isAdult', { is: true, then: (schema) => schema.required() }), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/CheckboxImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { CheckboxImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: CheckboxImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const Checkbox = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/SuidFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { SuidFormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'MUI Implementations', 6 | component: SuidFormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const SuidForm = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/SetFieldValueOptions.ts: -------------------------------------------------------------------------------- 1 | export type SetFieldValueOptions = { 2 | validate?: boolean; 3 | silentValidation?: boolean; 4 | touch?: boolean; 5 | dirty?: boolean; 6 | htmlElement?: HTMLElement; 7 | validateOn?: string[]; 8 | delay?: number; 9 | forceValidate?: boolean; 10 | mapValue?: (value: any) => any; 11 | }; 12 | -------------------------------------------------------------------------------- /www/src/code-snippets/formHasChanges1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | console.log(formHandler.formHasChanges()); // false 3 | formHandler.setFieldValue('name', 'John'); 4 | console.log(formHandler.formHasChanges()); // true 5 | 6 | //By setting back to the initial value of name 7 | formHandler.setFieldValue('name', ''); 8 | console.log(formHandler.formHasChanges()); // false 9 | -------------------------------------------------------------------------------- /www/src/code-snippets/resetForm1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.fillForm({ name: 'John', age: 28 }); 3 | console.log(formHandler.formData()); 4 | /** 5 | * -- Output: -- 6 | * {name: 'John', age: 28} 7 | */ 8 | 9 | formHandler.resetForm(); 10 | console.log(formHandler.formData()); 11 | /** 12 | * -- Output: -- 13 | * {name: '', age: ''} 14 | */ 15 | -------------------------------------------------------------------------------- /.github/workflows/actions/lib-publish-to-npm/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish lib to npm 2 | description: Publish lib to npm 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Publish package on NPM 7 | run: npm publish 8 | shell: bash 9 | working-directory: ./packages/lib 10 | env: 11 | NODE_AUTH_TOKEN: ${{ inputs.npm-token }} 12 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/zod/Checkbox/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | subscribed: z.literal(true, { 5 | errorMap: () => ({ message: 'subscribed is a required field' }), 6 | }), 7 | status: z.enum(['enabled', 'disabled']), 8 | }); 9 | 10 | export type Schema = z.infer; 11 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/NestedDeepFieldImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { NestedDeepFieldImpl } from '.'; 4 | 5 | const meta = { 6 | component: NestedDeepFieldImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const NestedDeepField = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/utils/reorderArray/index.test.ts: -------------------------------------------------------------------------------- 1 | import { reorderArray } from '@utils'; 2 | 3 | describe('reorderArray', () => { 4 | it('CASE-1', () => { 5 | expect(reorderArray([1, 2, 3], 1, 0)).toMatchObject([2, 1, 3]); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(reorderArray([1, 2, 3], 2, 0)).toMatchObject([3, 1, 2]); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /www/src/code-snippets/formHasChanges2.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | console.log(formHandler.formHasChanges()); // false 3 | formHandler.fillForm({ name: 'John', age: 28 }); 4 | console.log(formHandler.formHasChanges()); // true 5 | 6 | //By setting back to the initial values 7 | formHandler.fillForm({ name: '', age: '' }); 8 | console.log(formHandler.formHasChanges()); // false 9 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/Checkbox/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | subscribed: z.literal(true, { 5 | errorMap: () => ({ message: 'subscribed is a required field' }), 6 | }), 7 | status: z.enum(['enabled', 'disabled']), 8 | }); 9 | 10 | export type Schema = z.infer; 11 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/CheckboxesImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { CheckboxesImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: CheckboxesImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const Checkboxes = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ReferralsForm/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { ReferralsForm } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: ReferralsForm, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const ReferralsFormImpl = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/ValidationSchema.ts: -------------------------------------------------------------------------------- 1 | export type ValidationSchema = { 2 | isFieldFromSchema: (path: string) => boolean; 3 | validateAt: (path: string, data: T, options?: { recursive?: boolean; abortEarly?: boolean }) => Promise; 4 | buildDefault: (schema?: any, path?: string, object?: T) => any; 5 | getFieldDataType: (path: string) => string; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ComplexFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { ComplexFormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: ComplexFormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const ComplexForm = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/MultiSelectImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { MultiSelectImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: MultiSelectImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const MultiSelect = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/NestedFieldImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { NestedFieldImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: NestedFieldImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const NestedField = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ValidateOnImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { ValidateOnImpl } from '.'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: ValidateOnImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const ValidateOn = {}; 13 | -------------------------------------------------------------------------------- /www/src/utils/flattenTree/mocks.ts: -------------------------------------------------------------------------------- 1 | import { TreeMenu } from '@interfaces'; 2 | 3 | export const MENU: TreeMenu[] = [ 4 | { 5 | text: 'Getting started', 6 | section: true, 7 | children: [{ text: 'Introduction', route: 'introduction' }], 8 | }, 9 | { 10 | text: 'Validations', 11 | route: 'validations', 12 | section: true, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/Radio/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | gender: 'male' | 'female' | 'other'; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | gender: yup.mixed().required().oneOf(['male', 'female', 'other'], 'gender is a required field'), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/CheckboxCompForm/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { CheckboxCompForm } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: CheckboxCompForm, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const FormWithCheckbox = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FieldsetsFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { FieldsetsFormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: FieldsetsFormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const FieldsetsForm = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/utils/ValidationError/index.test.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '@utils'; 2 | 3 | describe('ValidationResult', () => { 4 | it('Must generate validation result object', () => { 5 | const validationResult = new ValidationError('field', 'field has error'); 6 | expect(validationResult).toMatchObject({ path: 'field', message: 'field has error' }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /.github/workflows/actions/www-build-and-test/action.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | description: Docs build and testing 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Install dependencies and build 7 | run: pnpm install --no-frozen-lockfile && pnpm build && pnpm build-www 8 | shell: bash 9 | - name: Tests 10 | run: pnpm test-www 11 | shell: bash 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/NestedArrFieldImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { NestedFieldArrImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: NestedFieldArrImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const NestedFieldArr = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ValidateOnZodImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { ValidateOnZodImpl } from '.'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: ValidateOnZodImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const ValidateOnZod = {}; 13 | -------------------------------------------------------------------------------- /packages/lib/src/utils/FormErrorsException/index.test.ts: -------------------------------------------------------------------------------- 1 | import { FormErrorsException } from '@utils'; 2 | 3 | describe('FormErrorsException', () => { 4 | it('Must show the form error validations', () => { 5 | const error = new FormErrorsException([{ path: 'name', message: 'Name is required' }]); 6 | expect(error.validationResult[0].message).toBe('Name is required'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/AsyncValidationImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { AsyncValidationImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: AsyncValidationImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const AsyncValidation = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/VanillaCompFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { VanillaCompFormImpl } from '.'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: VanillaCompFormImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const VanillaForm = {}; 13 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/yup/context.ts: -------------------------------------------------------------------------------- 1 | import { FormHandler } from 'solid-form-handler'; 2 | import { createContext, useContext } from 'solid-js'; 3 | import { Schema } from './types'; 4 | 5 | export const FormContext = createContext( 6 | {} as { 7 | formHandler: FormHandler; 8 | } 9 | ); 10 | 11 | export const useFormContext = () => useContext(FormContext); 12 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/zod/context.ts: -------------------------------------------------------------------------------- 1 | import { FormHandler } from 'solid-form-handler'; 2 | import { createContext, useContext } from 'solid-js'; 3 | import { Schema } from './types'; 4 | 5 | export const FormContext = createContext( 6 | {} as { 7 | formHandler: FormHandler; 8 | } 9 | ); 10 | 11 | export const useFormContext = () => useContext(FormContext); 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FieldsetsFormStress1/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { FieldsetsFormStress1 } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: FieldsetsFormStress1, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const FieldsetsFormStress = {}; 12 | -------------------------------------------------------------------------------- /www/src/components/Code/index.css: -------------------------------------------------------------------------------- 1 | .code-snippet { 2 | margin-bottom: 17px; 3 | } 4 | 5 | .code-snippet pre { 6 | margin-bottom: 0px; 7 | } 8 | 9 | .code-snippet pre code { 10 | font-size: 16px; 11 | line-height: 26px; 12 | padding: 20px; 13 | } 14 | 15 | .code-snippet.no-border { 16 | border: 0 !important; 17 | } 18 | 19 | .code-snippet.loading { 20 | min-height: 150px; 21 | } 22 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/yup/types.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { 2 | step1: { 3 | firstName: string; 4 | secondName: string; 5 | gender: 'male' | 'female' | 'other'; 6 | }; 7 | step2: { 8 | university: number; 9 | profession: number; 10 | country: number; 11 | }; 12 | step3: { 13 | contact: Array<{ email: string; phone?: string }>; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ValidateFileInputImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { ValidateFileInputImpl } from '.'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: ValidateFileInputImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const ValidateFileInput = {}; 13 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ComplexNestedFieldsImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { ComplexNestedFieldsImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: ComplexNestedFieldsImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const ComplexNestedFields = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/DependantValidationImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { DependantValidationImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: DependantValidationImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const DependantValidation = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/NestedFieldsetsFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { NestedFieldsetsFormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: NestedFieldsetsFormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const NestedFieldsetsForm = {}; 12 | -------------------------------------------------------------------------------- /www/src/code-snippets/schemaYup2.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | type Company = { 3 | name: string; 4 | contact: { 5 | email: string; 6 | phone: string; 7 | }; 8 | }; 9 | 10 | const companySchema: yup.Schema = yup.object({ 11 | name: yup.string().required(), 12 | contact: yup.object({ 13 | email: yup.string().email().required(), 14 | phone: yup.string().required(), 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/code-snippets/touchField1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | 6 | formHandler.setFieldValue(name, value) 7 | } 8 | onBlur={({ currentTarget: { name } }) => { 9 | formHandler.validateField(name); 10 | formHandler.touchField(name); //Marks the field as touched 11 | }} 12 | >; 13 | -------------------------------------------------------------------------------- /www/src/implementations/PersonForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const personSchema = z.object({ 4 | name: z.string().min(1), 5 | age: z.coerce.number().min(1, 'number is a required field'), 6 | contact: z.object({ 7 | email: z.string().email(), 8 | phone: z.string().min(1, 'phone is required'), 9 | address: z.string().min(1, 'address is required'), 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/SortableFieldsetsFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { SortableFieldsetsFormImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: SortableFieldsetsFormImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const SortableFieldsetsForm = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/TemperatureConversionImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { TemperatureConversionImpl } from '.'; 3 | 4 | const meta = { 5 | title: 'BS5 Implementations', 6 | component: TemperatureConversionImpl, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | 11 | export const TemperatureConversion = {}; 12 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FieldState.ts: -------------------------------------------------------------------------------- 1 | export type FieldState = { 2 | dataType: string; 3 | errorMessage: string; 4 | isFieldset: boolean; 5 | isInvalid: boolean; 6 | htmlElement?: HTMLElement; 7 | defaultValue: any; 8 | initialValue: any; 9 | currentValue: any; 10 | touched: boolean; 11 | dirty: boolean; 12 | triggers: string[]; 13 | validating: boolean; 14 | validationId: string; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/NestedPaths.ts: -------------------------------------------------------------------------------- 1 | import { IsTuple, TupleKeys, Paths } from '@interfaces'; 2 | 3 | export type NestedPaths = T extends ReadonlyArray 4 | ? IsTuple extends true 5 | ? { 6 | [K in TupleKeys]: Paths; 7 | }[TupleKeys] 8 | : Paths 9 | : { 10 | [K in keyof T]-?: Paths; 11 | }[keyof T]; 12 | -------------------------------------------------------------------------------- /packages/lib/src/utils/getFieldsPaths/index.ts: -------------------------------------------------------------------------------- 1 | import { ENDS_WITH_DOT_NUMBER_REGEXP, ROOT_KEY } from '@constants'; 2 | import { isInteger, objectPaths } from '@utils'; 3 | 4 | export const getFieldsPaths = (data: any) => { 5 | const paths = objectPaths(data).filter( 6 | (path) => !path.match(ENDS_WITH_DOT_NUMBER_REGEXP)?.length && !isInteger(path) 7 | ); 8 | paths.unshift(ROOT_KEY); 9 | return paths; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/zod/Select/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | country: z.coerce.number().min(1, 'country is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const COUNTRIES = [ 10 | { value: 1, label: 'Colombia' }, 11 | { value: 2, label: 'Argentina' }, 12 | { value: 3, label: 'Venezuela' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/Select/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | country: z.coerce.number().min(1, 'country is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const COUNTRIES = [ 10 | { value: 1, label: 'Colombia' }, 11 | { value: 2, label: 'Argentina' }, 12 | { value: 3, label: 'Venezuela' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/lib/src/utils/formatObjectPath/index.test.ts: -------------------------------------------------------------------------------- 1 | import { formatObjectPath } from '@utils'; 2 | 3 | describe('formatObjectPath', () => { 4 | it('formats brackets path notation to dots', () => { 5 | expect(formatObjectPath('[0].key1')).toBe('0.key1'); 6 | expect(formatObjectPath('["0"].key1[2]')).toBe('0.key1.2'); 7 | expect(formatObjectPath('[0]["1"].key1["3"][2].key5')).toBe('0.1.key1.3.2.key5'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldChildrenPath/index.ts: -------------------------------------------------------------------------------- 1 | import { CHILDREN_KEY, ENDS_WITH_DOT_STATE_REGEXP } from '@constants'; 2 | import { buildFieldStatePath } from '@utils'; 3 | 4 | /** 5 | * Function that build the field children path when is given a normal path. 6 | */ 7 | export const buildFieldChildrenPath = (path: string) => { 8 | return buildFieldStatePath(path)?.replace?.(ENDS_WITH_DOT_STATE_REGEXP, `.${CHILDREN_KEY}`); 9 | }; 10 | -------------------------------------------------------------------------------- /www/src/implementations/ValidationDelayForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | name: z.string().min(1, 'Required field'), 5 | email: z 6 | .string() 7 | .email() 8 | .refine(async (value) => { 9 | return new Promise((res) => { 10 | setTimeout(() => res(value !== 'test@mail.com'), 200); 11 | }); 12 | }, 'Email test@mail.com already exists'), 13 | }); 14 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/zod/Checkboxes/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | favoriteFoods: z.array(z.number()).min(1, 'favoriteFoods is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const FAVORITE_FOODS = [ 10 | { value: 1, label: 'Pizza' }, 11 | { value: 2, label: 'Hot Dog' }, 12 | { value: 3, label: 'Ice Cream' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/lib/src/utils/ValidationError/index.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMap } from '@interfaces'; 2 | 3 | export class ValidationError extends Error { 4 | path: string; 5 | children: ErrorMap = []; 6 | 7 | constructor(path: string, message: string, children: ErrorMap = []) { 8 | super(message); 9 | this.path = path; 10 | this.message = message; 11 | this.children = children; 12 | this.name = 'ValidationError'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/Checkboxes/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | favoriteFoods: z.array(z.number()).min(1, 'favoriteFoods is a required field'), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const FAVORITE_FOODS = [ 10 | { value: 1, label: 'Pizza' }, 11 | { value: 2, label: 'Hot Dog' }, 12 | { value: 3, label: 'Ice Cream' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /.github/workflows/actions/lib-build-and-test/action.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | description: Library build and testing 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Install dependencies and build 7 | run: pnpm install --no-frozen-lockfile && pnpm build 8 | shell: bash 9 | working-directory: ./packages/lib 10 | - name: Tests 11 | run: pnpm test 12 | shell: bash 13 | working-directory: ./packages/lib 14 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/yup/Checkbox/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | subscribed: boolean; 5 | status: 'enabled' | 'disabled'; 6 | }; 7 | 8 | export const schema: yup.Schema = yup.object().shape({ 9 | subscribed: yup.boolean().required().oneOf([true], 'subscribed is a required field'), 10 | status: yup.mixed().oneOf(['enabled', 'disabled']).required(), 11 | }); 12 | -------------------------------------------------------------------------------- /www/src/implementations/ProductsForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Product } from './types'; 3 | 4 | export const productSchema: yup.Schema = yup 5 | .array( 6 | yup.object({ 7 | name: yup.string().required('Required field'), 8 | quantity: yup 9 | .number() 10 | .required('Quantity is required') 11 | .typeError('Write a valid quantity'), 12 | }) 13 | ) 14 | .required(); 15 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/Checkbox/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | subscribed: boolean; 5 | status: 'enabled' | 'disabled'; 6 | }; 7 | 8 | export const schema: yup.Schema = yup.object().shape({ 9 | subscribed: yup.boolean().required().oneOf([true], 'subscribed is a required field'), 10 | status: yup.mixed().oneOf(['enabled', 'disabled']).required(), 11 | }); 12 | -------------------------------------------------------------------------------- /www/src/components/Navbar/index.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | height: var(--navbar-height); 3 | } 4 | 5 | .right-menu { 6 | font-size: 30px; 7 | line-height: 20px; 8 | } 9 | 10 | @media (max-width: 991px) { 11 | .right-menu { 12 | display: none; 13 | } 14 | 15 | .navbar { 16 | height: auto; 17 | min-height: var(--navbar-height); 18 | } 19 | } 20 | 21 | @media (max-width: 340px) { 22 | .navbar-brand { 23 | font-size: 16px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /www/src/implementations/PersonForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Person } from './types'; 3 | 4 | export const personSchema: yup.Schema = yup.object({ 5 | name: yup.string().required(), 6 | age: yup.number().required().typeError('number is a required field'), 7 | contact: yup.object({ 8 | email: yup.string().required(), 9 | phone: yup.string().required(), 10 | address: yup.string().required(), 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /www/src/implementations/ReferralsForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const referralsSchema = z.object({ 4 | hostName: z.string().min(1, 'hostName is required'), 5 | hostEmail: z.string().email(), 6 | referrals: z 7 | .array( 8 | z.object({ 9 | name: z.string().min(1, 'name is required'), 10 | email: z.string().email(), 11 | }) 12 | ) 13 | .min(1, 'At least one referral must be added'), 14 | }); 15 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/zod/Radios/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | gender: z.enum(['male', 'female', 'other'], { errorMap: () => ({ message: 'gender is a required field' }) }), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const GENDERS = [ 10 | { value: 'male', label: 'Male' }, 11 | { value: 'female', label: 'Female' }, 12 | { value: 'other', label: 'Other' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/yup/Select/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | country: number; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | country: yup.number().required().typeError('country is a required field'), 9 | }); 10 | 11 | export const COUNTRIES = [ 12 | { value: 1, label: 'Colombia' }, 13 | { value: 2, label: 'Argentina' }, 14 | { value: 3, label: 'Venezuela' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/Select/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | country: number; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | country: yup.number().required().typeError('country is a required field'), 9 | }); 10 | 11 | export const COUNTRIES = [ 12 | { value: 1, label: 'Colombia' }, 13 | { value: 2, label: 'Argentina' }, 14 | { value: 3, label: 'Venezuela' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/zod/Radios/mocks.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | gender: z.enum(['male', 'female', 'other'], { errorMap: () => ({ message: 'gender is a required field' }) }), 5 | }); 6 | 7 | export type Schema = z.infer; 8 | 9 | export const GENDERS = [ 10 | { value: 'male', label: 'Male' }, 11 | { value: 'female', label: 'Female' }, 12 | { value: 'other', label: 'Other' }, 13 | ]; 14 | -------------------------------------------------------------------------------- /www/src/components/Tabs/index.css: -------------------------------------------------------------------------------- 1 | .tabs-wrapper .tab-content .tab-content { 2 | border: 0 !important; 3 | } 4 | 5 | .tabs-wrapper .tabs-wrapper { 6 | padding-top: 17px; 7 | } 8 | 9 | .tabs-wrapper .tabs-wrapper.mb-4 { 10 | margin-bottom: 0 !important; 11 | } 12 | 13 | .tabs .nav-tabs { 14 | flex-wrap: nowrap; 15 | } 16 | 17 | .tabs .nav-tabs .nav-item { 18 | cursor: pointer; 19 | white-space: nowrap; 20 | } 21 | 22 | .tab-content .code-snippet { 23 | margin-bottom: 0px; 24 | } 25 | -------------------------------------------------------------------------------- /www/src/implementations/ConditionalValidationForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z 4 | .object({ 5 | isAdult: z.boolean(), 6 | email: z.string().email().optional().or(z.literal('')), 7 | }) 8 | .superRefine((data, ctx) => { 9 | if (data.isAdult === true && data?.email?.length === 0) { 10 | ctx.addIssue({ 11 | code: 'custom', 12 | path: ['email'], 13 | message: 'Email is required', 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/implementations/SuidUserForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const userSchema = z.object({ 4 | name: z.string().min(1, 'Required field'), 5 | email: z.string().email(), 6 | country: z.number().min(1, 'Country is required'), 7 | favoriteFoods: z.array(z.number()).min(2), 8 | gender: z 9 | .string() 10 | .refine((value) => 11 | ['male', 'female', 'other'].some((item) => item === value) 12 | ), 13 | subscribed: z.boolean().default(false), 14 | }); 15 | -------------------------------------------------------------------------------- /www/src/implementations/UserForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const userSchema = z.object({ 4 | name: z.string().min(1, 'Required field'), 5 | email: z.string().email('Invalid email'), 6 | country: z.number().min(1, 'Country is required'), 7 | favoriteFoods: z.array(z.number()).min(2), 8 | gender: z 9 | .string() 10 | .refine((value) => 11 | ['male', 'female', 'other'].some((item) => item === value) 12 | ), 13 | subscribed: z.boolean(), 14 | }); 15 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/SelectImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { SelectImpl } from '.'; 3 | import { ySchema, zSchema } from './schemas'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: SelectImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const SelectWithYup = () => ; 13 | export const SelectWithZod = () => ; 14 | -------------------------------------------------------------------------------- /packages/lib/src/utils/get/index.ts: -------------------------------------------------------------------------------- 1 | import { formatObjectPath } from '@utils'; 2 | 3 | /** 4 | * Gets the value from a nested object path. 5 | */ 6 | export const get = (data: any, path: string): T => { 7 | path = formatObjectPath(path); 8 | const [key, ...rest] = path.split('.'); 9 | 10 | if (!key) return data; 11 | 12 | const value = data[key]; 13 | 14 | if (typeof data[key] === 'object') { 15 | return get(value, rest.join('.')); 16 | } else { 17 | return value; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/FieldStore.ts: -------------------------------------------------------------------------------- 1 | import { CommonEvent, SetFieldValueOptions, ValidateFieldOptions } from '@interfaces'; 2 | 3 | export type FieldStore = { 4 | props: { 5 | value?: any; 6 | id?: string; 7 | name?: string; 8 | onBlur?: CommonEvent; 9 | }; 10 | helpers: { 11 | errorMessage: string; 12 | error: boolean; 13 | onValueChange: (value: any, options?: SetFieldValueOptions) => void; 14 | onFieldBlur: (options?: ValidateFieldOptions) => void; 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /www/src/code-snippets/validateForm1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | try { 3 | await formHandler.validateForm(); 4 | } catch (error) { 5 | if (error instanceof FormErrorsException) { 6 | console.log(error); 7 | /** 8 | * -- Output: -- 9 | * [ 10 | * { 11 | * path: 'name', 12 | * message: 'name is a required field' 13 | * }, 14 | * { 15 | * path: 'age', 16 | * message: 'age is a required field' 17 | * }, 18 | * ] 19 | */ 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/yup/Checkboxes/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | favoriteFoods: Array; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | favoriteFoods: yup.array(yup.number().required()).min(1, 'favoriteFoods is a required field').required(), 9 | }); 10 | 11 | export const FAVORITE_FOODS = [ 12 | { value: 1, label: 'Pizza' }, 13 | { value: 2, label: 'Hot Dog' }, 14 | { value: 3, label: 'Ice Cream' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/Checkboxes/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | favoriteFoods: Array; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | favoriteFoods: yup.array(yup.number().required()).min(1, 'favoriteFoods is a required field').required(), 9 | }); 10 | 11 | export const FAVORITE_FOODS = [ 12 | { value: 1, label: 'Pizza' }, 13 | { value: 2, label: 'Hot Dog' }, 14 | { value: 3, label: 'Ice Cream' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/SelectImpl/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { yupSchema, zodSchema } from '@adapters'; 3 | import { z } from 'zod'; 4 | 5 | const _ySchema: yup.Schema<{ 6 | country: number; 7 | }> = yup.object().shape({ 8 | country: yup.number().required().typeError('Country is required'), 9 | }); 10 | 11 | export const ySchema = yupSchema(_ySchema); 12 | export const zSchema = zodSchema( 13 | z.object({ 14 | country: z.coerce.number().min(1, 'Country is required'), 15 | }) 16 | ); 17 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldParentPath/index.tsx: -------------------------------------------------------------------------------- 1 | import { ROOT_KEY } from '@constants'; 2 | import { isInteger } from '@utils'; 3 | 4 | export const buildFieldParentPath = (path: string) => { 5 | const arrPath = path.split('.'); 6 | let builtPath = ROOT_KEY; 7 | arrPath.pop(); 8 | 9 | for (let i = arrPath.length - 1; i >= 0; i--) { 10 | if (!isInteger(arrPath[i])) { 11 | builtPath = arrPath.join('.'); 12 | break; 13 | } 14 | 15 | arrPath.pop(); 16 | } 17 | 18 | return builtPath; 19 | }; 20 | -------------------------------------------------------------------------------- /www/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@jest/types'; 2 | import { pathsToModuleNameMapper } from 'ts-jest'; 3 | import { compilerOptions } from './tsconfig.json'; 4 | 5 | const config: Config.InitialOptions = { 6 | verbose: true, 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '', 9 | }), 10 | transform: { 11 | '\\.[jt]sx?$': 'babel-jest', 12 | }, 13 | testEnvironment: 'jsdom', 14 | preset: 'solid-jest/preset/browser', 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FileInputImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { FileInputImpl } from '.'; 3 | import { ySchema, zSchema } from './schemas'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: FileInputImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const FileInputImplWithYup = () => ; 13 | export const FileInputImplWithZod = () => ; 14 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/TextInputImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | import { TextInputImpl } from '.'; 3 | import { ySchema, zSchema } from './schemas'; 4 | 5 | const meta = { 6 | title: 'BS5 Implementations', 7 | component: TextInputImpl, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const TextInputImplWithYup = () => ; 13 | export const TextInputImplWithZod = () => ; 14 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldParentPath/index.test.ts: -------------------------------------------------------------------------------- 1 | import { ROOT_KEY } from '@constants'; 2 | import { buildFieldParentPath } from '@utils'; 3 | 4 | describe('getParentPath', () => { 5 | it('CASE-1', () => { 6 | expect(buildFieldParentPath('key1')).toBe(`${ROOT_KEY}`); 7 | }); 8 | 9 | it('CASE-2', () => { 10 | expect(buildFieldParentPath('key1.0.name')).toBe(`key1`); 11 | }); 12 | 13 | it('CASE-3', () => { 14 | expect(buildFieldParentPath('key1.0.contact.0.1')).toBe(`key1.0.contact`); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './Code'; 4 | export * from './CodeTabs'; 5 | export * from './FileInput'; 6 | export * from './Implementation'; 7 | export * from './Navbar'; 8 | export * from './Radio'; 9 | export * from './Radios'; 10 | export * from './Redirect'; 11 | export * from './Select'; 12 | export * from './Sidebar'; 13 | export * from './SidebarMenu'; 14 | export * from './SidebarProvider'; 15 | export * from './Tabs'; 16 | export * from './TextInput'; 17 | -------------------------------------------------------------------------------- /packages/lib/examples/components/legacy/__tests__/yup/Radios/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | gender: 'male' | 'female' | 'other'; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | gender: yup.mixed().required().oneOf(['male', 'female', 'other'], 'gender is a required field'), 9 | }); 10 | 11 | export const GENDERS = [ 12 | { value: 'male', label: 'Male' }, 13 | { value: 'female', label: 'Female' }, 14 | { value: 'other', label: 'Other' }, 15 | ]; 16 | -------------------------------------------------------------------------------- /www/src/implementations/PasswordConfirmForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z 4 | .object({ 5 | password: z.string().min(1, 'Password is required'), 6 | passwordConfirm: z.string().min(1, 'Password confirm is required'), 7 | }) 8 | .superRefine((data, ctx) => { 9 | if (data.password !== data.passwordConfirm) { 10 | ctx.addIssue({ 11 | code: 'custom', 12 | path: ['password', 'passwordConfirm'], 13 | message: "Password doesn't match", 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/implementations/ReferralsForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Referrals } from './types'; 3 | 4 | export const referralsSchema: yup.Schema = yup.object({ 5 | hostName: yup.string().required(), 6 | hostEmail: yup.string().email().required(), 7 | referrals: yup 8 | .array( 9 | yup.object({ 10 | name: yup.string().required(), 11 | email: yup.string().email().required(), 12 | }) 13 | ) 14 | .required() 15 | .min(1, 'At least one referral must be added'), 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/components/Redirect/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useRouteData } from '@solidjs/router'; 2 | import { Component, onMount } from 'solid-js'; 3 | 4 | export interface RedirectProps { 5 | href?: string; 6 | } 7 | 8 | export const Redirect: Component = (props) => { 9 | const routeData = useRouteData(); 10 | const navigate = useNavigate(); 11 | 12 | onMount(() => { 13 | const href = props.href || routeData.href; 14 | href && navigate(href, { replace: true }); 15 | }); 16 | 17 | return <>; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ReferralsForm/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Referrals } from './types'; 3 | 4 | export const referralsSchema: yup.Schema = yup.object({ 5 | hostName: yup.string().required(), 6 | hostEmail: yup.string().email().required(), 7 | referrals: yup 8 | .array( 9 | yup.object({ 10 | name: yup.string().required(), 11 | email: yup.string().email().required(), 12 | }) 13 | ) 14 | .min(1, 'At least one referral must be added') 15 | .required(), 16 | }); 17 | -------------------------------------------------------------------------------- /www/src/code-snippets/fieldHasError1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.fieldHasError('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.fieldHasError('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.fieldHasError('contacts.0.email'); 9 | formHandler.fieldHasError('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.fieldHasError('0.contact.email'); 13 | formHandler.fieldHasError('0.contacts.0.email'); 14 | formHandler.fieldHasError('[0]contact.email'); 15 | formHandler.fieldHasError('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /www/src/code-snippets/getFieldError1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.getFieldError('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.getFieldError('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.getFieldError('contacts.0.email'); 9 | formHandler.getFieldError('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.getFieldError('0.contact.email'); 13 | formHandler.getFieldError('0.contacts.0.email'); 14 | formHandler.getFieldError('[0]contact.email'); 15 | formHandler.getFieldError('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /www/src/code-snippets/getFieldValue1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.getFieldValue('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.getFieldValue('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.getFieldValue('contacts.0.email'); 9 | formHandler.getFieldValue('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.getFieldValue('0.contact.email'); 13 | formHandler.getFieldValue('0.contacts.0.email'); 14 | formHandler.getFieldValue('[0]contact.email'); 15 | formHandler.getFieldValue('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /www/src/code-snippets/validateField1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.validateField('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.validateField('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.validateField('contacts.0.email'); 9 | formHandler.validateField('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.validateField('0.contact.email'); 13 | formHandler.validateField('0.contacts.0.email'); 14 | formHandler.validateField('[0]contact.email'); 15 | formHandler.validateField('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/CheckboxImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { CheckboxImpl } from '.'; 3 | 4 | describe('Checkbox use case', () => { 5 | it('must render error message on change when value is empty', async () => { 6 | render(() => ); 7 | fireEvent.change(screen.getByTestId('test-checkbox'), { target: { value: false } }); 8 | await waitFor(() => { 9 | expect(screen.getByText('Field must be checked')).toBeDefined(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isEmpty/index.ts: -------------------------------------------------------------------------------- 1 | import { flattenObject } from '@utils'; 2 | 3 | export const isEmpty = (data: any) => { 4 | if (data === undefined || data === '') return true; 5 | if (typeof data === 'object') { 6 | const flattenedObject = flattenObject(data); 7 | if (Object.keys(flattenedObject).length === 0) return true; 8 | for (let key in flattenedObject) { 9 | const value = flattenedObject[key]; 10 | if (value !== '' && value !== undefined) return false; 11 | } 12 | return true; 13 | } 14 | 15 | return false; 16 | }; 17 | -------------------------------------------------------------------------------- /www/src/code-snippets/formData1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | const formHandler = useFormHandler(__VALIDATOR__Schema(userSchema)); 3 | const { formData } = formHandler; 4 | 5 | console.log(formData().name); //'' 6 | console.log(formData().age); //'' 7 | console.log(formData()); 8 | /** 9 | * -- Output: -- 10 | * {name: '', age: ''} 11 | */ 12 | 13 | formHandler.fillForm({ name: 'John', age: 28 }); 14 | console.log(formData().name); //John 15 | console.log(formData().age); //28 16 | console.log(formData()); 17 | /** 18 | * -- Output: -- 19 | * {name: 'John', age: 28} 20 | */ 21 | -------------------------------------------------------------------------------- /www/src/pages/Api/ResetForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const ResetForm: Component = () => ( 6 | <> 7 |

resetForm

8 |

9 | This method resets the form to its initial state leaving empty the whole 10 | form data. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/code-snippets/isFieldInvalid1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.isFieldInvalid('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.isFieldInvalid('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.isFieldInvalid('contacts.0.email'); 9 | formHandler.isFieldInvalid('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.isFieldInvalid('0.contact.email'); 13 | formHandler.isFieldInvalid('0.contacts.0.email'); 14 | formHandler.isFieldInvalid('[0]contact.email'); 15 | formHandler.isFieldInvalid('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /www/src/pages/Api/GetFieldDefaultValue/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const GetFieldDefaultValue: Component = () => ( 6 | <> 7 |

getFieldDefaultValue

8 |

This method obtains the default value of a field from the form state.

9 | 10 |

11 | Implementation: 12 |

13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ConditionalFormImpl/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from 'storybook-solidjs'; 2 | 3 | import { ConditionalFormImpl } from '.'; 4 | import { ySchema, zSchema } from './schemas'; 5 | 6 | const meta = { 7 | title: 'BS5 Implementations', 8 | component: ConditionalFormImpl, 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | 13 | export const ConditionalFormWithYup = () => ; 14 | export const ConditionalFormWithZod = () => ; 15 | -------------------------------------------------------------------------------- /packages/lib/src/utils/clone/index.ts: -------------------------------------------------------------------------------- 1 | import { flattenObject, set } from '@utils'; 2 | 3 | export const clone = (data: T): T | undefined => { 4 | let obj: any = undefined; 5 | if (data === undefined) return obj; 6 | if (typeof data !== 'object') return data; 7 | if (Array.isArray(data)) obj = []; 8 | if (!Array.isArray(data)) obj = {}; 9 | 10 | const flattenedObject = flattenObject(data); 11 | 12 | Object.keys(flattenedObject).forEach((path) => { 13 | const value = flattenedObject[path]; 14 | set(obj, path, value); 15 | }); 16 | 17 | return obj; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/lib/src/utils/objectPaths/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves all the object paths in a recursive way. 3 | */ 4 | export const objectPaths = (data: any, path: string = '', paths: Array = []) => { 5 | path = path ? `${path}.` : ''; 6 | 7 | if (typeof data === 'object') { 8 | Object.keys(data).forEach((key) => { 9 | let nextPath = `${path}${key}`; 10 | 11 | if (typeof data[key] === 'object') { 12 | objectPaths(data[key], nextPath, paths); 13 | } 14 | 15 | paths.unshift(nextPath); 16 | }); 17 | } 18 | 19 | return paths; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/lib/examples/components/__tests__/yup/Radios/mocks.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export type Schema = { 4 | gender: 'male' | 'female' | 'other'; 5 | }; 6 | 7 | export const schema: yup.Schema = yup.object().shape({ 8 | gender: yup 9 | .mixed() 10 | .required() 11 | .oneOf(['male', 'female', 'other'], 'gender is a required field') 12 | .required(), 13 | }); 14 | 15 | export const GENDERS = [ 16 | { value: 'male', label: 'Male' }, 17 | { value: 'female', label: 'Female' }, 18 | { value: 'other', label: 'Other' }, 19 | ]; 20 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/TextInputImpl/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { yupSchema, zodSchema } from '@adapters'; 3 | import { z } from 'zod'; 4 | 5 | const _ySchema: yup.Schema<{ 6 | name: string; 7 | age: number; 8 | }> = yup.object().shape({ 9 | name: yup.string().required(), 10 | age: yup.number().required().min(1), 11 | }); 12 | 13 | export const ySchema = yupSchema(_ySchema); 14 | export const zSchema = zodSchema( 15 | z.object({ 16 | name: z.string().min(1, 'name is a required field'), 17 | age: z.number().min(1), 18 | }) 19 | ); 20 | -------------------------------------------------------------------------------- /www/src/code-snippets/useFormHandler3.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | const formHandler = useFormHandler(__VALIDATOR__Schema(userSchema), { 6 | //Time given in milliseconds. 7 | delay: 1000, 8 | }); 9 | 10 | /** 11 | * Value is set immediately but the validation will be 12 | * debounced by 1 second (1000 milliseconds) 13 | */ 14 | formHandler.setFieldValue('name', 'John'); 15 | 16 | //Validation also will be debounced. 17 | formHandler.validateField('name'); 18 | -------------------------------------------------------------------------------- /www/src/pages/Api/MoveFieldset/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const MoveFieldset: Component = () => ( 6 | <> 7 |

moveFieldset

8 |

9 | Method for manipulating dynamic forms. It lets to move a fieldset inside 10 | the array of fieldsets. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/pages/Api/AddFieldset/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const AddFieldset: Component = () => ( 6 | <> 7 |

addFieldset

8 |

9 | Method for manipulating dynamic forms. It adds a subset of fields when the 10 | form is an array of fieldsets. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/pages/Api/GetFieldError/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const GetFieldError: Component = () => ( 6 | <> 7 |

getFieldError

8 |

9 | This method returns a string with the field error message. This 10 | information is get from the form state. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/pages/Api/SetFieldTriggers/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const SetFieldTriggers: Component = () => ( 6 | <> 7 |

setFieldTriggers

8 |

9 | Method for setting fields validations that depends on the current field 10 | validation. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/pages/Api/FormIsFilling/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@components'; 2 | import { getRaw } from '@utils'; 3 | import { Component } from 'solid-js'; 4 | 5 | export const FormIsFilling: Component = () => { 6 | return ( 7 | <> 8 |

formIsFilling

9 |

10 | Boolean signal triggered when fillForm method is executed. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /www/src/pages/Api/RemoveFieldset/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const RemoveFieldset: Component = () => ( 6 | <> 7 |

removeFieldset

8 |

9 | Method for manipulating dynamic forms. It lets to remove a fieldset inside 10 | from the array of fieldsets. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/utils/flattenTree/index.test.ts: -------------------------------------------------------------------------------- 1 | import { flattenTree } from './index'; 2 | import { MENU } from './mocks'; 3 | 4 | describe('flattenTree', () => { 5 | it('Flattens a nested tree data structure', () => { 6 | const flattenedTree = flattenTree(MENU); 7 | 8 | expect(flattenedTree).toMatchObject([ 9 | { 10 | text: 'Getting started', 11 | section: true, 12 | }, 13 | { text: 'Introduction', route: 'introduction' }, 14 | { 15 | text: 'Validations', 16 | route: 'validations', 17 | section: true, 18 | }, 19 | ]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /www/src/pages/Docs/Setup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from '@components'; 2 | import { Component } from 'solid-js'; 3 | import { SetupCmd } from './SetupCmd'; 4 | 5 | export const Setup: Component = () => { 6 | return ( 7 | <> 8 |

Setup

9 | , 14 | }, 15 | { 16 | text: 'zod', 17 | children: , 18 | }, 19 | ]} 20 | /> 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /www/src/code-snippets/getFieldDefaultValue1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | formHandler.getFieldDefaultValue('age'); 3 | 4 | //For nested objects you can access by dot notation 5 | formHandler.getFieldDefaultValue('contact.email'); 6 | 7 | //For nested arrays too 8 | formHandler.getFieldDefaultValue('contacts.0.email'); 9 | formHandler.getFieldDefaultValue('contacts[0]email'); 10 | 11 | //And fieldsets 12 | formHandler.getFieldDefaultValue('0.contact.email'); 13 | formHandler.getFieldDefaultValue('0.contacts.0.email'); 14 | formHandler.getFieldDefaultValue('[0]contact.email'); 15 | formHandler.getFieldDefaultValue('[0]contacts[0]email'); 16 | -------------------------------------------------------------------------------- /www/src/pages/Api/FormIsResetting/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@components'; 2 | import { getRaw } from '@utils'; 3 | import { Component } from 'solid-js'; 4 | 5 | export const FormIsResetting: Component = () => { 6 | return ( 7 | <> 8 |

formIsResetting

9 |

10 | Boolean signal triggered when resetForm method is executed. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /www/src/pages/Api/GetFieldValue/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const GetFieldValue: Component = () => ( 6 | <> 7 |

getFieldValue

8 |

9 | This method obtains the current value of a field. The value is obtained 10 | from the formData() reactive object. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/utils/flattenTree/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Util function for flattening a nested tree data structure 3 | */ 4 | import { Tree, Flatten } from '@interfaces'; 5 | import { clone } from '../clone'; 6 | 7 | export const flattenTree = >>( 8 | data: Tree> = [], 9 | flattenedTree: Tree> = [] 10 | ): Tree> => { 11 | clone(data).forEach((item) => { 12 | flattenedTree.push(item); 13 | 14 | if (item.children) { 15 | flattenTree(item.children, flattenedTree); 16 | delete item.children; 17 | } 18 | }); 19 | 20 | return flattenedTree; 21 | }; 22 | -------------------------------------------------------------------------------- /www/src/pages/Api/IsFieldInvalid/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const IsFieldInvalid: Component = () => ( 6 | <> 7 |

isFieldInvalid

8 |

9 | This method returns a boolean flag, true if the field is 10 | invalid. This information is get from the form state. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /www/src/implementations/SuidUserForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { User } from './types'; 3 | 4 | export const userSchema: yup.Schema = yup.object({ 5 | name: yup.string().required('Required field'), 6 | email: yup.string().email('Invalid email').required('Email is required'), 7 | country: yup.number().required().typeError('Country is required'), 8 | favoriteFoods: yup.array(yup.number().required()).required().min(2), 9 | gender: yup 10 | .mixed() 11 | .oneOf(['male', 'female', 'other']) 12 | .required(), 13 | subscribed: yup.boolean().required().default(false), 14 | }); 15 | -------------------------------------------------------------------------------- /www/src/implementations/UserForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { User } from './types'; 3 | 4 | export const userSchema: yup.Schema = yup.object({ 5 | name: yup.string().required('Required field'), 6 | email: yup.string().email('Invalid email').required('Email is required'), 7 | country: yup.number().required().typeError('Country is required'), 8 | favoriteFoods: yup.array(yup.number().required()).required().min(2), 9 | gender: yup 10 | .mixed() 11 | .oneOf(['male', 'female', 'other']) 12 | .required(), 13 | subscribed: yup.boolean().required().default(false), 14 | }); 15 | -------------------------------------------------------------------------------- /www/src/pages/Docs/Setup/SetupCmd/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@components'; 2 | import { Component } from 'solid-js'; 3 | 4 | export interface SetupCmdProps { 5 | validatorLib: 'yup' | 'zod'; 6 | } 7 | 8 | export const SetupCmd: Component = (props) => ( 9 |
10 |
11 | npm installation: 12 | {`npm i solid-form-handler ${props.validatorLib}`} 13 |
14 |
15 | yarn installation: 16 | {`yarn add solid-form-handler ${props.validatorLib}`} 17 |
18 |
19 | ); 20 | -------------------------------------------------------------------------------- /www/src/apis/ComponentFieldApi.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | function Field(props: FieldComponentProps): JSXElement; 3 | 4 | type FieldComponentProps = CommonFieldProps & 5 | ( 6 | | InputFieldProps 7 | | CheckboxFieldProps 8 | | CheckboxGroupFieldProps 9 | | RadioGroupFieldProps 10 | ); 11 | 12 | interface CommonFieldProps extends FieldProps { 13 | onBlur?: CommonEvent; 14 | onBlurOptions?: ValidateFieldOptions; 15 | } 16 | 17 | type FieldProps = { 18 | id?: string; 19 | error?: boolean; 20 | errorMessage?: string; 21 | formHandler?: FormHandler; 22 | name?: string; 23 | triggers?: string[]; 24 | value?: any; 25 | }; 26 | -------------------------------------------------------------------------------- /www/src/pages/Api/FormIsValidating/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@components'; 2 | import { getRaw } from '@utils'; 3 | import { Component } from 'solid-js'; 4 | 5 | export const FormIsValidating: Component = () => { 6 | return ( 7 | <> 8 |

formIsValidating

9 |

10 | Boolean signal triggered when validateForm method is 11 | executed. 12 |

13 | 14 |

15 | Implementation: 16 |

17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /www/src/pages/Api/TouchField/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const TouchField: Component = () => ( 6 | <> 7 |

touchField

8 |

9 | This method marks the field as touched to determine if the user has 10 | interacted with the form component. A true flag is stored at 11 | form state. 12 |

13 | 14 |

15 | Implementation: 16 |

17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/lib/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import { Bootstrap5Theme } from './decorators'; 2 | 3 | export const parameters = { 4 | backgrounds: { 5 | default: 'light', 6 | }, 7 | actions: { argTypesRegex: '^on[A-Z].*' }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | options: { 15 | storySort: { 16 | method: 'alphabetical', 17 | }, 18 | }, 19 | }; 20 | 21 | export const decorators = [ 22 | (Story, { kind }) => { 23 | if (kind.match('BS5')) { 24 | return Bootstrap5Theme({ children: Story() }); 25 | } 26 | 27 | return Story(); 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /packages/lib/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@jest/types'; 2 | import { pathsToModuleNameMapper } from 'ts-jest'; 3 | import { compilerOptions } from './tsconfig.json'; 4 | 5 | const config: Config.InitialOptions = { 6 | verbose: true, 7 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 8 | prefix: '', 9 | }), 10 | globals: { 11 | 'ts-jest': { 12 | isolatedModules: true, 13 | }, 14 | }, 15 | transform: { 16 | '\\.[jt]sx?$': 'babel-jest', 17 | }, 18 | testMatch: ['**/?(*.)(test).ts?(x)'], 19 | testEnvironment: 'jsdom', 20 | preset: 'solid-jest/preset/browser', 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /www/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render, Suspense } from 'solid-js/web'; 2 | import { Router, hashIntegration } from '@solidjs/router'; 3 | import { App } from './App'; 4 | import './index.css'; 5 | import 'bootstrap/dist/js/bootstrap.js'; 6 | import { SidebarProvider } from '@components'; 7 | import { loadSnippets } from '@utils'; 8 | import { BASE_URL } from '@constants'; 9 | 10 | loadSnippets(); 11 | 12 | render( 13 | () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ), 22 | document.getElementById('root') as HTMLElement 23 | ); 24 | -------------------------------------------------------------------------------- /www/src/pages/Api/IsFormInvalid/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const IsFormInvalid: Component = () => ( 6 | <> 7 |

isFormInvalid

8 |

9 | This method retrieves a boolean flag. It’s true if the form contains 10 | invalid fields and false when all the fields are valid. 11 |

12 | 13 |

14 | Implementation: 15 |

16 |

Form will be invalid if it doesn't satisfy the schema constraints:

17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /www/src/pages/Api/FieldHasError/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const FieldHasError: Component = () => ( 6 | <> 7 |

fieldHasError

8 |

9 | This method returns a boolean flag, true if the field has an 10 | error message. This method is different from isFieldInvalid, 11 | a field can be invalid but not contains an error message. 12 |

13 | 14 |

15 | Implementation: 16 |

17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /www/src/code-snippets/setFieldTriggersApi1.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | /** 3 | * Password field triggers passwordConfirm validation only if the user 4 | * has interacted with both fields. 5 | */ 6 | formHandler.setFieldTriggers('password', ['passwordConfirm']); 7 | 8 | /** 9 | * passwordConfirm field triggers password validation only if the user 10 | * has interacted with both fields. 11 | */ 12 | formHandler.setFieldTriggers('passwordConfirm', ['password']); 13 | 14 | formHandler.setFieldValue('password', 'ab'); //Won't trigger passwordConfirm validation 15 | formHandler.setFieldValue('passwordConfirm', 'abc'); //Triggers password validation 16 | formHandler.setFieldValue('password', 'ab'); //Triggers passwordConfirm validation 17 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/DependantValidationImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { DependantValidationImpl } from '.'; 3 | 4 | describe('Dependant validations impl.', () => { 5 | it('each field must render error message on blur', async () => { 6 | render(() => ); 7 | fireEvent.blur(screen.getByTestId('test-password')); 8 | fireEvent.blur(screen.getByTestId('test-passwordConfirm')); 9 | await waitFor(() => { 10 | expect(screen.getByText('password is a required field')).toBeDefined(); 11 | expect(screen.getByText('passwordConfirm is a required field')).toBeDefined(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /www/src/implementations/PasswordConfirmForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Schema } from './types'; 3 | 4 | export const schema: yup.Schema = yup.object({ 5 | password: yup 6 | .string() 7 | .required() 8 | .test({ 9 | name: 'matches', 10 | test: (value, context) => { 11 | return value == context.parent['passwordConfirm']; 12 | }, 13 | message: "Password doesn't match", 14 | }), 15 | passwordConfirm: yup 16 | .string() 17 | .required() 18 | .test({ 19 | name: 'matches', 20 | test: (value, context) => { 21 | return value == context.parent['password']; 22 | }, 23 | message: "Password doesn't match", 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /www/src/implementations/ValidationDelayForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Schema } from './types'; 3 | 4 | export const schema: yup.Schema = yup.object({ 5 | name: yup.string().required('Required field'), 6 | email: yup 7 | .string() 8 | .email() 9 | .required('Required field') 10 | .test('emailExists', (value, context) => { 11 | return new Promise((res, rej) => { 12 | setTimeout(() => { 13 | if (value !== 'test@mail.com') { 14 | res(true); 15 | } else { 16 | rej( 17 | context.createError({ message: `Email ${value} already exists.` }) 18 | ); 19 | } 20 | }, 200); 21 | }); 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /www/src/pages/Api/FormHasChanges/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const FormHasChanges: Component = () => ( 6 | <> 7 |

formHasChanges

8 |

9 | This method returns a boolean flag, true if the form has 10 | changes. 11 |

12 | 13 |

14 | Implementation: 15 |

16 |

Form detects changes when setting a field value:

17 | 18 |

Also when filling a form:

19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /.github/workflows/www.yml: -------------------------------------------------------------------------------- 1 | name: Docs www pipeline 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - '*' 7 | paths: 8 | - www/** 9 | push: 10 | branches: 11 | - master 12 | paths: 13 | - www/** 14 | defaults: 15 | run: 16 | working-directory: www 17 | jobs: 18 | www-workflow: 19 | runs-on: ubuntu-latest 20 | name: WWW workflow 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Setup Node 25 | uses: ./.github/workflows/actions/setup-node 26 | - name: Setup pnpm 27 | uses: ./.github/workflows/actions/setup-pnpm 28 | - name: Build and test 29 | uses: ./.github/workflows/actions/www-build-and-test 30 | -------------------------------------------------------------------------------- /packages/lib/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormErrorsException'; 2 | export * from './ValidationError'; 3 | export * from './buildFieldChildrenPath'; 4 | export * from './buildFieldParentPath'; 5 | export * from './buildFieldStatePath'; 6 | export * from './buildFieldStatePaths'; 7 | export * from './clone'; 8 | export * from './createStore'; 9 | export * from './equals'; 10 | export * from './flattenObject'; 11 | export * from './formatObjectPath'; 12 | export * from './get'; 13 | export * from './getFieldsPaths'; 14 | export * from './isEmpty'; 15 | export * from './isInteger'; 16 | export * from './isNumber'; 17 | export * from './objectPaths'; 18 | export * from './objectValueExists'; 19 | export * from './reorderArray'; 20 | export * from './set'; 21 | -------------------------------------------------------------------------------- /www/src/layouts/MainLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { NavLink, Outlet } from '@solidjs/router'; 3 | import { Navbar } from '@components'; 4 | import { MAIN_MENU, MAIN_RIGHT_MENU } from '@constants'; 5 | import logo from '@images/logo.svg'; 6 | import './index.css'; 7 | 8 | export const MainLayout: Component = () => { 9 | return ( 10 | <> 11 | 14 | 15 | solid-form-handler 16 | 17 | } 18 | menu={MAIN_MENU} 19 | rightMenu={MAIN_RIGHT_MENU} 20 | /> 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldStatePaths/index.ts: -------------------------------------------------------------------------------- 1 | import { ENDS_WITH_DOT_STATE_REGEXP, ROOT_KEY } from '@constants'; 2 | import { buildFieldStatePath, getFieldsPaths, objectPaths } from '@utils'; 3 | 4 | /** 5 | * Obtains recursively the object paths. 6 | */ 7 | export const buildFieldStatePaths = (data: any) => { 8 | const fieldsPaths = getFieldsPaths(data); 9 | const fieldStatePaths: string[] = []; 10 | 11 | if (typeof data === 'object') { 12 | fieldsPaths.forEach((fieldPath) => { 13 | const fieldStatePath = buildFieldStatePath(fieldPath); 14 | 15 | if (fieldStatePath?.match(ENDS_WITH_DOT_STATE_REGEXP)) { 16 | fieldStatePaths.push(fieldStatePath); 17 | } 18 | }); 19 | } 20 | 21 | return { fieldStatePaths, fieldsPaths }; 22 | }; 23 | -------------------------------------------------------------------------------- /www/src/pages/Api/IsFieldValidating/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@components'; 2 | import { getRaw } from '@utils'; 3 | import { Component } from 'solid-js'; 4 | 5 | export const IsFieldValidating: Component = () => { 6 | return ( 7 | <> 8 |

isFieldValidating

9 |

10 | Form field boolean signal triggered when validateForm or{' '} 11 | validateField methods are executed. Useful for adding a 12 | loading spinner on async form field validations. 13 |

14 | 15 |

16 | Implementation: 17 |

18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ValidateFileInputImpl/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { useFormHandler } from '@hooks'; 3 | import { yupSchema } from '@adapters'; 4 | import { FileInput } from '@example-components'; 5 | import * as yup from 'yup'; 6 | 7 | type Schema = { 8 | name: string; 9 | }; 10 | 11 | const schema: yup.Schema = yup.object().shape({ 12 | name: yup.string().required(), 13 | }); 14 | 15 | export const ValidateFileInputImpl: Component = () => { 16 | const formHandler = useFormHandler(yupSchema(schema)); 17 | 18 | return ( 19 | <> 20 |
21 |

File input implementation

22 | 23 |
24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/lib/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from 'storybook-solidjs-vite'; 2 | import { mergeConfig } from 'vite'; 3 | import viteTsConfigPaths from 'vite-tsconfig-paths'; 4 | 5 | const config: StorybookConfig = { 6 | stories: ['../examples/**/*.mdx', '../examples/**/*.stories.@(js|jsx|ts|tsx)'], 7 | addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], 8 | framework: { 9 | name: 'storybook-solidjs-vite', 10 | options: {}, 11 | }, 12 | docs: { 13 | autodocs: 'tag', 14 | }, 15 | viteFinal: async (config) => { 16 | // Merge custom configuration into the default config 17 | return mergeConfig(config, { 18 | plugins: [viteTsConfigPaths()], 19 | }); 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /packages/lib/src/utils/objectValueExists/index.test.ts: -------------------------------------------------------------------------------- 1 | import { objectValueExists } from '@utils'; 2 | 3 | describe('isNumber', () => { 4 | it('CASE-1', () => { 5 | expect(objectValueExists([], '0')).toBe(false); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(objectValueExists(['Hi'], '0')).toBe(true); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(objectValueExists([{ greet: 'Hi' }], '0.greet')).toBe(true); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(objectValueExists({ greet: 'Hi' }, 'greet')).toBe(true); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect(objectValueExists({ greet: '' }, 'greet')).toBe(true); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect(objectValueExists({ greet: '' }, 'anyKey')).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/lib/src/utils/set/index.ts: -------------------------------------------------------------------------------- 1 | import { formatObjectPath } from '@utils'; 2 | 3 | export const set = (object: any, path: string, value: any) => { 4 | const arrPath = formatObjectPath(path).split('.'); 5 | const [firstKey] = arrPath; 6 | 7 | if (object === undefined && !isNaN(firstKey as any)) object = []; 8 | if (object === undefined && isNaN(firstKey as any)) object = {}; 9 | 10 | let obj = object; 11 | 12 | arrPath.forEach((key, i) => { 13 | const nextKey = arrPath[i + 1]; 14 | if (obj[key] === undefined && isNaN(nextKey as any)) obj[key] = {}; 15 | if (obj[key] === undefined && !isNaN(nextKey as any)) obj[key] = []; 16 | if (nextKey !== undefined) obj = obj[key]; 17 | if (nextKey === undefined) obj[key] = value; 18 | }); 19 | 20 | return object; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldChildrenPath/index.test.ts: -------------------------------------------------------------------------------- 1 | import { buildFieldChildrenPath } from '@utils'; 2 | import { CHILDREN_KEY, ROOT_KEY } from '@constants'; 3 | 4 | describe('buildFieldChildrenPath', () => { 5 | it('CASE-1', () => { 6 | expect(buildFieldChildrenPath('key1.key2.key3')).toBe( 7 | `${ROOT_KEY}.${CHILDREN_KEY}.key1.${CHILDREN_KEY}.key2.${CHILDREN_KEY}.key3.${CHILDREN_KEY}` 8 | ); 9 | }); 10 | 11 | it('CASE-2', () => { 12 | expect(buildFieldChildrenPath('0.1.key1')).toBe(undefined); 13 | }); 14 | 15 | it('CASE-3', () => { 16 | expect(buildFieldChildrenPath('0.1.2')).toBe(undefined); 17 | }); 18 | 19 | it('CASE-4', () => { 20 | expect(buildFieldChildrenPath('key1.0')).toBe(`${ROOT_KEY}.${CHILDREN_KEY}.key1.${CHILDREN_KEY}`); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /www/src/pages/Api/GetFormErrors/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const GetFormErrors: Component = () => ( 6 | <> 7 |

getFormErrors

8 |

This method retrieves each field error message from the form.

9 | 10 |

11 | Implementation: 12 |

13 | 14 |

15 | FormFieldError is composed by: 16 |

17 |
    18 |
  • 19 | path: field name. 20 |
  • 21 |
  • 22 | errorMessage: field error. 23 |
  • 24 |
25 | 26 | ); 27 | -------------------------------------------------------------------------------- /packages/lib/src/utils/clone/index.test.ts: -------------------------------------------------------------------------------- 1 | import { clone } from '@utils'; 2 | 3 | describe('clone', () => { 4 | it('CASE-1', () => { 5 | expect(clone({})).toMatchObject({}); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(clone([])).toMatchObject([]); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(clone('')).toBe(''); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(clone(100)).toBe(100); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect([{ name: 'Laura' }]).toMatchObject([{ name: 'Laura' }]); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect({ name: 'Laura' }).toMatchObject({ name: 'Laura' }); 26 | }); 27 | 28 | it('CASE-7', () => { 29 | expect({ contacts: [{ phone: 111 }] }).toMatchObject({ contacts: [{ phone: 111 }] }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /www/src/implementations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CheckboxCompForm'; 2 | export * from './CheckboxForm'; 3 | export * from './CheckboxesCompForm'; 4 | export * from './CheckboxesForm'; 5 | export * from './ConditionalValidationForm'; 6 | export * from './FileInputCompForm'; 7 | export * from './MultiStepForm'; 8 | export * from './PasswordConfirmForm'; 9 | export * from './PersonForm'; 10 | export * from './ProductsForm'; 11 | export * from './RadiosCompForm'; 12 | export * from './RadiosForm'; 13 | export * from './ReferralsForm'; 14 | export * from './SelectCompForm'; 15 | export * from './SingleSelectForm'; 16 | export * from './SingleTextInputForm'; 17 | export * from './SuidUserForm'; 18 | export * from './TextInputCompForm'; 19 | export * from './UserForm'; 20 | export * from './ValidateOnForm'; 21 | export * from './ValidationDelayForm'; 22 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingTextInput1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { Field, useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | // ... 6 | 7 | const formHandler = useFormHandler(__VALIDATOR__Schema(schema)); 8 | 9 | // ... 10 | ( 15 | <> 16 | 19 | 24 | 25 |
{field.helpers.errorMessage}
26 |
27 | 28 | )} 29 | />; 30 | -------------------------------------------------------------------------------- /www/src/code-snippets/useFormHandler2.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | const formHandler = useFormHandler(__VALIDATOR__Schema(userSchema), { 6 | //Events are an array of strings so you can set custom event types. 7 | validateOn: ['input'], 8 | }); 9 | 10 | //Value will be set and validated if matches input event 11 | formHandler.setFieldValue('name', 'John', { validateOn: ['input'] }); 12 | 13 | //If no event given, it will be set and validated 14 | formHandler.setFieldValue('name', 'Laura'); 15 | 16 | /** 17 | * Value will be set but wont be validated on blur event because the form handler 18 | * is configured for just checking the input event. 19 | */ 20 | formHandler.setFieldValue('name', 'John', { validateOn: ['blur'] }); 21 | -------------------------------------------------------------------------------- /www/src/pages/Docs/Validations/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | 3 | export const Validations: Component = () => { 4 | return ( 5 | <> 6 |

Validations

7 |

8 | Each HTML form component must be threatened in a different way to 9 | validate its value. The following implementations show how to use the 10 | form handler at the most common vanilla HTML form components like text 11 | input, select, a single checkbox, or a group of checkboxes and radio 12 | buttons. 13 |

14 |

15 | For styling form components during validations, will be used Bootstrap 16 | 5. Remember that you can adapt the given implementations to your CSS 17 | framework or any UI library of preference. 18 |

19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/src/utils/equals/index.ts: -------------------------------------------------------------------------------- 1 | import { flattenObject } from '@utils'; 2 | 3 | export const equals = (value1: any, value2: any) => { 4 | if (typeof value1 !== typeof value2) return false; 5 | 6 | if (typeof value1 === 'object' && typeof value2 === 'object') { 7 | const flattenedValue1 = flattenObject(value1); 8 | const flattenedValue2 = flattenObject(value2); 9 | 10 | if (Object.keys(flattenedValue1).length !== Object.keys(flattenedValue2).length) return false; 11 | if (Object.keys(flattenedValue1).length === 0 && Object.keys(flattenedValue2).length === 0) return true; 12 | 13 | for (let key in flattenedValue1) { 14 | if (equals(flattenedValue1[key], flattenedValue2[key]) === false) return false; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | if (value1 === value2) { 21 | return true; 22 | } 23 | 24 | return false; 25 | }; 26 | -------------------------------------------------------------------------------- /www/src/pages/Api/SetFieldDefaultValue/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const SetFieldDefaultValue: Component = () => ( 6 | <> 7 |

setFieldDefaultValue

8 |

9 | Sets the default field value which will be used when the form is 10 | initialized or reset. No validation is triggered. 11 |

12 | 13 |

14 | Implementation: 15 |

16 | 17 |

18 | options are composed by: 19 |

20 |
    21 |
  • 22 | mapValue: receives a function for mapping the default 23 | value. 24 |
  • 25 |
26 | 27 | ); 28 | -------------------------------------------------------------------------------- /packages/lib/src/utils/flattenObject/index.ts: -------------------------------------------------------------------------------- 1 | import { CommonObject } from '@interfaces'; 2 | import { formatObjectPath } from '@utils'; 3 | 4 | /** 5 | * Converts a nested object into a plain data structure. 6 | */ 7 | const addToObject = (data: any, path: string, flattenedObject: CommonObject) => { 8 | path = formatObjectPath(path); 9 | flattenedObject[path] = data; 10 | }; 11 | 12 | export const flattenObject = (data: any, path: string = '', flattenedObject: CommonObject = {}) => { 13 | if (Array.isArray(data) && data.length === 0 && path) { 14 | addToObject(data, path, flattenedObject); 15 | } else if (typeof data === 'object') { 16 | for (let key in data) { 17 | flattenedObject = flattenObject(data[key], `${path}.${key}`, flattenedObject); 18 | } 19 | } else { 20 | addToObject(data, path, flattenedObject); 21 | } 22 | 23 | return flattenedObject; 24 | }; 25 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | Solid Form Handler - The SolidJS form engine for validating forms 15 | 16 | 17 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FieldsetsFormImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { FieldsetsFormImpl } from '.'; 3 | 4 | describe('FieldsetsFormImpl', () => { 5 | it('adds a fieldset', async () => { 6 | render(() => ); 7 | fireEvent.click(screen.getByTestId('add')); 8 | fireEvent.click(screen.getByTestId('add')); 9 | await waitFor(() => { 10 | expect(screen.getAllByTestId('fieldset').length).toBe(3); 11 | }); 12 | }); 13 | 14 | it('removes a fieldset', async () => { 15 | render(() => ); 16 | fireEvent.click(screen.getByTestId('add')); 17 | fireEvent.click(screen.getByTestId('add')); 18 | fireEvent.click(screen.getByTestId('remove-2')); 19 | await waitFor(() => { 20 | expect(screen.getAllByTestId('fieldset').length).toBe(2); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/ConditionalFormImpl/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { yupSchema, zodSchema } from '@adapters'; 3 | import { z } from 'zod'; 4 | 5 | const _ySchema: yup.Schema<{ 6 | isAdult: boolean; 7 | email?: string; 8 | }> = yup.object({ 9 | isAdult: yup.boolean().required(), 10 | email: yup.string().when('isAdult', { is: true, then: (schema) => schema.required() }), 11 | }); 12 | 13 | export const ySchema = yupSchema(_ySchema); 14 | export const zSchema = zodSchema( 15 | z 16 | .object({ 17 | isAdult: z.boolean(), 18 | email: z.string().email().optional().or(z.literal('')), 19 | }) 20 | .superRefine((data, ctx) => { 21 | if (data.isAdult === true && data?.email?.length === 0) { 22 | ctx.addIssue({ 23 | code: 'custom', 24 | path: ['email'], 25 | message: 'Email is required', 26 | }); 27 | } 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /www/src/code-snippets/formData2.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | const { formData } = formHandler; 3 | 4 | console.log(formData().name); //'' 5 | console.log(formData().contact); //{ email: '', phone: '' } 6 | console.log(formData().contact.email); //'' 7 | console.log(formData().contact.phone); //'' 8 | console.log(formData()); 9 | /** 10 | * -- Output: -- 11 | * { name: '', contact: { email: '', phone: '' } } 12 | */ 13 | 14 | formHandler.fillForm({ 15 | name: 'Google', 16 | contact: { email: 'google@gmail.com', phone: '1112223' }, 17 | }); 18 | console.log(formData().name); //'Google' 19 | console.log(formData().contact); //{ email: 'google@gmail.com', phone: '1112223' } 20 | console.log(formData().contact.email); //'google@gmail.com' 21 | console.log(formData().contact.phone); //'1112223' 22 | console.log(formData()); 23 | /** 24 | * -- Output: -- 25 | * { name: 'Google', contact: { email: 'google@gmail.com', phone: '1112223' } } 26 | */ 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidjs-form-handler", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "www": "pnpm --filter www start", 8 | "publish-lib": "pnpm --filter solid-form-handler publish-lib", 9 | "local-publish-lib": "pnpm --filter solid-form-handler local-publish-lib", 10 | "storybook": "pnpm --filter solid-form-handler storybook", 11 | "build": "pnpm --filter solid-form-handler build", 12 | "build-www": "pnpm --filter www build", 13 | "serve-www": "pnpm --filter www serve", 14 | "test": "pnpm --filter solid-form-handler test", 15 | "test-www": "pnpm --filter www test", 16 | "test-core": "pnpm --filter solid-form-handler test-core", 17 | "sync-components": "pnpm --filter www sync-components", 18 | "deploy-docs": "pnpm --filter www deploy-docs" 19 | }, 20 | "keywords": [], 21 | "author": "Mauricio Rivera", 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /www/src/pages/Api/ValidateForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { getRaw } from '@utils'; 3 | import { Code } from '@components'; 4 | 5 | export const ValidateForm: Component = () => ( 6 | <> 7 |

validateForm

8 |

This method validates all the form fields.

9 | 10 |

11 | Implementation: 12 |

13 |

14 | When the form is invalid, it throws an error with the invalid fields error 15 | messages. 16 |

17 | 18 |

19 | FormErrorsException is an array composed by: 20 |

21 |
    22 |
  • 23 | path: field's name. 24 |
  • 25 |
  • 26 | message: field's validation error message. 27 |
  • 28 |
29 | 30 | ); 31 | -------------------------------------------------------------------------------- /www/src/pages/Docs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './Checkboxes'; 3 | export * from './Components'; 4 | export * from './DependantValidations'; 5 | export * from './DynamicForm'; 6 | export * from './DynamicNestedForm'; 7 | export * from './FileInput'; 8 | export * from './FormValidation'; 9 | export * from './Introduction'; 10 | export * from './MaterialUI'; 11 | export * from './NestedFormValidation'; 12 | export * from './QuickStart'; 13 | export * from './Radios'; 14 | export * from './Select'; 15 | export * from './Setup'; 16 | export * from './TextInput'; 17 | export * from './ValidateOn'; 18 | export * from './ValidatingCheckbox'; 19 | export * from './ValidatingCheckboxes'; 20 | export * from './ValidatingMultiStepForm'; 21 | export * from './ValidatingRadios'; 22 | export * from './ValidatingSelect'; 23 | export * from './ValidatingTextInput'; 24 | export * from './ValidationDelay'; 25 | export * from './Validations'; 26 | -------------------------------------------------------------------------------- /www/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, Plugin } from 'vite'; 2 | import copy from 'rollup-plugin-copy'; 3 | import solidPlugin from 'vite-plugin-solid'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | import dotenv from 'dotenv'; 6 | 7 | dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); 8 | 9 | export default defineConfig({ 10 | plugins: [solidPlugin(), tsconfigPaths({ root: __dirname })], 11 | server: { port: 4000 }, 12 | base: process.env.VITE_BASE_URL, 13 | build: { 14 | minify: false, 15 | target: 'esnext', 16 | rollupOptions: { 17 | plugins: [ 18 | copy({ 19 | copyOnce: true, 20 | hook: 'closeBundle', 21 | verbose: true, 22 | targets: [ 23 | { 24 | src: 'src/assets/images/**/*', 25 | dest: 'dist/images', 26 | }, 27 | ], 28 | }) as Plugin, 29 | ], 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/lib/src/constants/regexps.ts: -------------------------------------------------------------------------------- 1 | import { CHILDREN_KEY, ROOT_KEY, STATE_KEY } from '@constants'; 2 | 3 | export const ENDS_WITH_DOT_STATE_REGEXP = new RegExp(`\\.${STATE_KEY}$`); 4 | export const ENDS_WITH_DOT_CHILDREN_REGEXP = new RegExp(`\\.${CHILDREN_KEY}$`); 5 | export const ENDS_WITH_DOT_STATE_OR_DOT_CHILDREN_REGEXP = new RegExp(`(\\.${STATE_KEY}|\\.${CHILDREN_KEY}$)`, 'gi'); 6 | export const STARTS_WITH_ROOT_KEY_DOT_CHILDREN_REGEXP = new RegExp(`^${ROOT_KEY}\\.${CHILDREN_KEY}\\.`); 7 | export const IS_ROOT_KEY_DOT_STATE_REGEXP = new RegExp(`^${ROOT_KEY}\\.${STATE_KEY}$`); 8 | export const STARTS_WITH_NUMBER_DOT_REGEXP = new RegExp(`^\\d+\.`); 9 | export const CONTAINS_DOT_NUMBER_DOT_REGEXP = new RegExp(`\\.\\d+\\.`, 'g'); 10 | export const IS_INTEGER_REGEXP = new RegExp(`^[-]?\\d+\$`); 11 | export const ENDS_WITH_DOT_NUMBER_REGEXP = new RegExp(`\\.\\d+\$`); 12 | export const MATCHES_DOT_CHILDREN_DOT_KEY_REGEXP = new RegExp(`\\.${CHILDREN_KEY}\\.`, 'g'); 13 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/zod/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const schema = z.object({ 4 | step1: z 5 | .object({ 6 | firstName: z.string().min(1, 'firstName is required'), 7 | secondName: z.string().min(1, 'secondName is required'), 8 | gender: z 9 | .string() 10 | .refine((value) => 11 | ['male', 'female', 'other'].some((item) => item === value) 12 | ), 13 | }) 14 | .required(), 15 | step2: z 16 | .object({ 17 | university: z.coerce.number().min(1), 18 | profession: z.coerce.number().min(1), 19 | country: z.coerce.number().min(1), 20 | }) 21 | .required(), 22 | step3: z 23 | .object({ 24 | contact: z 25 | .array( 26 | z.object({ 27 | email: z.string().email(), 28 | phone: z.string(), 29 | }) 30 | ) 31 | .min(1), 32 | }) 33 | .required(), 34 | }); 35 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "preserve", 13 | "jsxImportSource": "solid-js", 14 | "paths": { 15 | "@components": ["src/components"], 16 | "@constants": ["src/constants"], 17 | "@images/*": ["src/assets/images/*"], 18 | "@implementations": ["src/implementations"], 19 | "@interfaces": ["src/interfaces"], 20 | "@layouts": ["src/layouts"], 21 | "@pages": ["src/pages"], 22 | "@routes": ["src/routes"], 23 | "@components/suid": ["src/components/suid"], 24 | "@utils": ["src/utils"] 25 | }, 26 | "types": ["vite/client", "node", "@types/jest"] 27 | }, 28 | "include": ["src"] 29 | } 30 | -------------------------------------------------------------------------------- /www/src/constants/navbarMenus.tsx: -------------------------------------------------------------------------------- 1 | import logo from '@images/logo.svg'; 2 | import npmLogo from '@images/npm-logo.png'; 3 | 4 | export const MAIN_MENU = [ 5 | { text: 'Docs', route: '/docs' }, 6 | { text: 'API', route: '/api' }, 7 | ]; 8 | 9 | export const MAIN_RIGHT_MENU = [ 10 | { 11 | icon: , 12 | route: 'https://github.com/webblocksapp/solid-form-handler', 13 | external: true, 14 | }, 15 | { 16 | icon: ( 17 | 22 | ), 23 | route: 'https://www.npmjs.com/package/solid-form-handler', 24 | external: true, 25 | }, 26 | { 27 | icon: ( 28 | 33 | ), 34 | route: 'https://www.solidjs.com/', 35 | external: true, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /packages/lib/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BrowserNativeObject'; 2 | export * from './CommonEvent'; 3 | export * from './CommonFieldProps'; 4 | export * from './CommonObject'; 5 | export * from './ErrorMap'; 6 | export * from './FieldPath'; 7 | export * from './FieldProps'; 8 | export * from './FieldState'; 9 | export * from './FieldStore'; 10 | export * from './Flatten'; 11 | export * from './FlattenPaths'; 12 | export * from './FormHandler'; 13 | export * from './FormHandlerOptions'; 14 | export * from './FormState'; 15 | export * from './FormStateUpdateBehavior'; 16 | export * from './IsTuple'; 17 | export * from './NestedPaths'; 18 | export * from './Paths'; 19 | export * from './Primitive'; 20 | export * from './SetFieldDefaultValueOptions'; 21 | export * from './SetFieldValueOptions'; 22 | export * from './TupleKeys'; 23 | export * from './ValidateFieldBehavior'; 24 | export * from './ValidateFieldOptions'; 25 | export * from './ValidateOptions'; 26 | export * from './ValidationSchema'; 27 | -------------------------------------------------------------------------------- /packages/lib/src/utils/getFieldsPaths/index.test.ts: -------------------------------------------------------------------------------- 1 | import { ROOT_KEY } from '@constants'; 2 | import { getFieldsPaths } from '@utils'; 3 | 4 | describe('getFieldsPaths', () => { 5 | it('CASE-1', () => { 6 | const value = getFieldsPaths([]); 7 | expect(value).toMatchObject([ROOT_KEY]); 8 | }); 9 | 10 | it('CASE-2', () => { 11 | const value = getFieldsPaths([{ name: 'John' }]); 12 | expect(value).toMatchObject([ROOT_KEY, '0.name']); 13 | }); 14 | 15 | it('CASE-3', () => { 16 | const value = getFieldsPaths([{ name: 'John', contacts: [{ email: 'mail@mail.com' }] }]); 17 | expect(value).toEqual(expect.arrayContaining([ROOT_KEY, '0.name', '0.contacts', '0.contacts.0.email'])); 18 | }); 19 | 20 | it('CASE-4', () => { 21 | const data = { key1: { key11: { key111: '' } }, key2: { key21: '' } }; 22 | expect(getFieldsPaths(data)).toEqual( 23 | expect.arrayContaining([ROOT_KEY, 'key1', 'key1.key11', 'key1.key11.key111', 'key2', 'key2.key21']) 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/yup/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { Schema } from './types'; 3 | 4 | export const schema: yup.Schema = yup.object({ 5 | step1: yup 6 | .object({ 7 | firstName: yup.string().required(), 8 | secondName: yup.string().required(), 9 | gender: yup 10 | .mixed() 11 | .required() 12 | .oneOf(['male', 'female', 'other']), 13 | }) 14 | .required(), 15 | step2: yup 16 | .object({ 17 | university: yup.number().required(), 18 | profession: yup.number().required(), 19 | country: yup.number().required(), 20 | }) 21 | .required(), 22 | step3: yup 23 | .object({ 24 | contact: yup 25 | .array( 26 | yup.object({ 27 | email: yup.string().email().required(), 28 | phone: yup.string().optional(), 29 | }) 30 | ) 31 | .required() 32 | .min(1), 33 | }) 34 | .required(), 35 | }); 36 | -------------------------------------------------------------------------------- /packages/lib/src/utils/buildFieldStatePath/index.test.ts: -------------------------------------------------------------------------------- 1 | import { buildFieldStatePath } from '@utils'; 2 | import { CHILDREN_KEY, ROOT_KEY, STATE_KEY } from '@constants'; 3 | 4 | describe('buildFieldStatePath', () => { 5 | it('CASE-1', () => { 6 | expect(buildFieldStatePath('key1.key2.key3')).toBe( 7 | `${ROOT_KEY}.${CHILDREN_KEY}.key1.${CHILDREN_KEY}.key2.${CHILDREN_KEY}.key3.${STATE_KEY}` 8 | ); 9 | }); 10 | 11 | it('CASE-2', () => { 12 | expect(buildFieldStatePath('0.1.key1')).toBe(undefined); 13 | }); 14 | 15 | it('CASE-3', () => { 16 | expect(buildFieldStatePath('0.1.2')).toBe(undefined); 17 | }); 18 | 19 | it('CASE-4', () => { 20 | expect(buildFieldStatePath('key1.0')).toBe(`${ROOT_KEY}.${CHILDREN_KEY}.key1.${STATE_KEY}`); 21 | }); 22 | 23 | it('CASE-5', () => { 24 | expect(buildFieldStatePath(ROOT_KEY)).toBe(`${ROOT_KEY}.${STATE_KEY}`); 25 | }); 26 | 27 | it('CASE-6', () => { 28 | expect(buildFieldStatePath('')).toBe(`${ROOT_KEY}.${STATE_KEY}`); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FormImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { FormImpl } from '.'; 3 | 4 | describe('Form implementation', () => { 5 | it('must display all error validations on submit', async () => { 6 | render(() => ); 7 | fireEvent.click(screen.getByTestId('submit')); 8 | await waitFor(() => { 9 | expect(screen.getByText('name is a required field')).toBeDefined(); 10 | expect(screen.getByText('Age is required')).toBeDefined(); 11 | }); 12 | }); 13 | 14 | it('no errors must be displayed on submit', async () => { 15 | render(() => ); 16 | fireEvent.input(screen.getByTestId('name'), { target: { value: 'John' } }); 17 | fireEvent.input(screen.getByTestId('age'), { target: { value: '28' } }); 18 | fireEvent.click(screen.getByTestId('submit')); 19 | await waitFor(async () => { 20 | expect(screen.getByTestId('error').innerHTML).toBe(''); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /www/src/components/SidebarMenu/index.css: -------------------------------------------------------------------------------- 1 | .sidebar-menu .nav-link { 2 | color: rgb(var(--nav-link)); 3 | } 4 | 5 | .sidebar-menu .nav-link.active { 6 | color: rgb(var(--bs-dark)); 7 | position: relative; 8 | } 9 | 10 | .sidebar-menu .nav-link.active::before { 11 | position: absolute; 12 | top: 0px; 13 | left: -17px; 14 | content: ''; 15 | width: 3px; 16 | height: 100%; 17 | background-color: rgb(var(--bs-primary-rgb)); 18 | display: block; 19 | } 20 | 21 | .sidebar-menu .nav-link:hover { 22 | color: rgb(var(--bs-dark)); 23 | } 24 | 25 | .sidebar-menu span.nav-link { 26 | cursor: default; 27 | } 28 | 29 | .sidebar-menu span.nav-link:hover { 30 | color: rgb(var(--nav-link)); 31 | } 32 | 33 | .sidebar-menu .section-item { 34 | font-size: 20px; 35 | color: rgb(var(--bs-primary-rgb)); 36 | font-weight: bolder; 37 | border-bottom: 1px solid #dee2e6; 38 | margin-bottom: 17px; 39 | margin-top: 17px; 40 | } 41 | 42 | .sidebar-menu span.section-item:hover { 43 | color: rgb(var(--bs-primary-rgb)); 44 | } 45 | -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "dist", 5 | "removeComments": false, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "jsx": "preserve", 12 | "jsxImportSource": "solid-js", 13 | "target": "esnext", 14 | "module": "esnext", 15 | "paths": { 16 | "@adapters": ["src/adapters"], 17 | "@components": ["src/components"], 18 | "@constants": ["src/constants"], 19 | "@hocs": ["src/hocs"], 20 | "@hooks": ["src/hooks"], 21 | "@interfaces": ["src/interfaces"], 22 | "@utils": ["src/utils"], 23 | "@example-components": ["examples/components"], 24 | "@example-components/suid": ["examples/components/suid"], 25 | "@example-components/legacy": ["examples/components/legacy"], 26 | "solid-form-handler": ["src"] 27 | }, 28 | "types": ["vite/client", "node", "@types/jest"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/lib/src/hocs/withFieldProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { FieldStore } from '@interfaces'; 2 | import { FieldComponentProps } from '@components'; 3 | import { Component, createContext, useContext } from 'solid-js'; 4 | import { createStore, SetStoreFunction } from 'solid-js/store'; 5 | 6 | const FieldContext = createContext( 7 | {} as { 8 | baseStore: FieldStore; 9 | setBaseStore: SetStoreFunction; 10 | } 11 | ); 12 | export const useFieldContext = () => useContext(FieldContext); 13 | 14 | export const withFieldProvider = (BaseComponent: Component) => { 15 | return (props: T) => { 16 | const [baseStore, setBaseStore] = createStore({ 17 | props: { id: '', name: '', value: '' }, 18 | helpers: { error: false, errorMessage: '', onValueChange: () => {}, onFieldBlur: () => {} }, 19 | }); 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /.github/workflows/lib.yml: -------------------------------------------------------------------------------- 1 | name: Library pipeline 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - '*' 7 | paths: 8 | - packages/lib/** 9 | push: 10 | branches: 11 | - master 12 | paths: 13 | - packages/lib/** 14 | release: 15 | types: [published] 16 | branches: 17 | - master 18 | defaults: 19 | run: 20 | working-directory: packages/lib 21 | jobs: 22 | lib-workflow: 23 | runs-on: ubuntu-latest 24 | name: Lib workflow 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | - name: Setup Node 29 | uses: ./.github/workflows/actions/setup-node 30 | - name: Setup pnpm 31 | uses: ./.github/workflows/actions/setup-pnpm 32 | - name: 'Build and test' 33 | uses: ./.github/workflows/actions/lib-build-and-test 34 | - name: 'Publish to npm' 35 | if: github.event_name == 'release' 36 | uses: ./.github/workflows/actions/lib-publish-to-npm 37 | with: 38 | npm-token: ${{ secrets.NPM_TOKEN }} 39 | -------------------------------------------------------------------------------- /packages/lib/src/utils/get/index.test.ts: -------------------------------------------------------------------------------- 1 | import { get } from '@utils'; 2 | 3 | describe('get', () => { 4 | it('CASE-1', () => { 5 | const value = get([], '0'); 6 | expect(value).toBe(undefined); 7 | }); 8 | 9 | it('CASE-2', () => { 10 | const value = get([{ name: 'John' }], '0.name'); 11 | expect(value).toBe('John'); 12 | }); 13 | 14 | it('CASE-3', () => { 15 | const value = get([{ emails: [] }], '0.emails'); 16 | expect(value).toMatchObject([]); 17 | }); 18 | 19 | it('CASE-4', () => { 20 | const value = get({ key1: { key2: { key3: 'Hello world' } } }, 'key1.key2.key3'); 21 | expect(value).toBe('Hello world'); 22 | }); 23 | 24 | it('CASE-5', () => { 25 | const obj = [{ name: 'Julia' }, { name: 'Leo' }]; 26 | const value1 = get(obj, '[0].name'); 27 | const value2 = get(obj, '1.name'); 28 | 29 | expect(value1).toBe('Julia'); 30 | expect(value2).toBe('Leo'); 31 | }); 32 | 33 | it('CASE-6', () => { 34 | const obj = { height: 2.23 }; 35 | expect(get(obj, 'height')).toBe(2.23); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/lib/examples/components/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { FieldProps, Field } from 'solid-form-handler'; 2 | import { Component, JSX, Show, splitProps } from 'solid-js'; 3 | 4 | export type TextInputProps = JSX.InputHTMLAttributes & FieldProps & { label?: string }; 5 | 6 | export const TextInput: Component = (props) => { 7 | const [local, rest] = splitProps(props, ['classList', 'label', 'formHandler']); 8 | 9 | return ( 10 | ( 14 |
15 | 16 | 19 | 20 | 21 | 22 |
{field.helpers.errorMessage}
23 |
24 |
25 | )} 26 | /> 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /www/src/components/SidebarProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accessor, 3 | Component, 4 | createContext, 5 | createSignal, 6 | JSX, 7 | useContext, 8 | } from 'solid-js'; 9 | 10 | const SidebarContext = createContext<{ 11 | active: Accessor; 12 | open: () => void; 13 | close: () => void; 14 | toggle: () => void; 15 | }>(); 16 | export const useSidebarContext = () => useContext(SidebarContext); 17 | 18 | export interface SidebarProviderProps { 19 | children: JSX.Element; 20 | } 21 | 22 | export const SidebarProvider: Component = (props) => { 23 | const [active, setActive] = createSignal(false); 24 | 25 | const open = () => { 26 | setActive(true); 27 | }; 28 | 29 | const close = () => { 30 | setActive(false); 31 | }; 32 | 33 | const toggle = () => { 34 | setActive((prev) => !prev); 35 | }; 36 | 37 | return ( 38 | 46 | {props.children} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingCheckbox1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { Field, useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | // ... 6 | 7 | const formHandler = useFormHandler(__VALIDATOR__Schema(schema)); 8 | 9 | // ... 10 | 11 | ( 16 | <> 17 |
23 | 31 | 34 |
35 | 36 |
{field.helpers.errorMessage}
37 |
38 | 39 | )} 40 | />; 41 | -------------------------------------------------------------------------------- /www/src/pages/Api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AddFieldset'; 2 | export * from './Field'; 3 | export * from './FieldCheckboxGroupMode'; 4 | export * from './FieldCheckboxMode'; 5 | export * from './FieldHasError'; 6 | export * from './FieldInputMode'; 7 | export * from './FieldRadioGroupMode'; 8 | export * from './FillForm'; 9 | export * from './FormData'; 10 | export * from './FormHasChanges'; 11 | export * from './FormIsFilling'; 12 | export * from './FormIsResetting'; 13 | export * from './FormIsValidating'; 14 | export * from './GetFieldDefaultValue'; 15 | export * from './GetFieldError'; 16 | export * from './GetFieldValue'; 17 | export * from './GetFormErrors'; 18 | export * from './IsFieldInvalid'; 19 | export * from './IsFieldValidating'; 20 | export * from './IsFormInvalid'; 21 | export * from './MoveFieldset'; 22 | export * from './RemoveFieldset'; 23 | export * from './ResetForm'; 24 | export * from './SetFieldDefaultValue'; 25 | export * from './SetFieldTriggers'; 26 | export * from './SetFieldValue'; 27 | export * from './TouchField'; 28 | export * from './UseFormHandler'; 29 | export * from './ValidateField'; 30 | export * from './ValidateForm'; 31 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingSelect1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { Field, useFormHandler } from 'solid-form-handler'; 3 | import { __VALIDATOR__Schema } from 'solid-form-handler/__VALIDATOR__'; 4 | 5 | // ... 6 | 7 | const formHandler = useFormHandler(__VALIDATOR__Schema(schema)); 8 | 9 | // ... 10 | 11 | ( 16 | <> 17 | 20 | 36 | 37 |
{field.helpers.errorMessage}
38 |
39 | 40 | )} 41 | />; 42 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingRadios1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | ( 7 | <> 8 | 9 | {(ageRange, i) => ( 10 |
16 | 27 | 30 |
31 | )} 32 |
33 | 34 |
{field.helpers.errorMessage}
35 |
36 | 37 | )} 38 | />; 39 | -------------------------------------------------------------------------------- /www/src/pages/Docs/QuickStart/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code, Tabs } from '@components'; 2 | import { NavLink } from '@solidjs/router'; 3 | import { getRaw } from '@utils'; 4 | import { Component } from 'solid-js'; 5 | 6 | export const QuickStart: Component = () => ( 7 | <> 8 |

Quick Start

9 |

10 | You can start by creating your own form field{' '} 11 | components by using each of the 12 | code given on the docs website. The following is a final implementation of 13 | them: 14 |

15 | 25 | ), 26 | }, 27 | { 28 | text: 'zod', 29 | children: ( 30 | 35 | ), 36 | }, 37 | ]} 38 | /> 39 | 40 | ); 41 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/TextInputImpl/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { TextInput } from '@example-components'; 3 | import { useFormHandler } from '@hooks'; 4 | import { ValidationSchema } from '@interfaces'; 5 | 6 | export const TextInputImpl: Component<{ schema: ValidationSchema }> = (props) => { 7 | const formHandler = useFormHandler(props.schema); 8 | 9 | const submit = async (event: Event) => { 10 | event.preventDefault(); 11 | try { 12 | await formHandler.validateForm(); 13 | alert(JSON.stringify(formHandler.formData())); 14 | formHandler.resetForm(); 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | }; 19 | 20 | return ( 21 | <> 22 |
23 |

Text input implementation

24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 |
32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/yup/Step1.tsx: -------------------------------------------------------------------------------- 1 | import { Radios, TextInput } from '@components'; 2 | import { Component } from 'solid-js'; 3 | import { useFormContext } from './context'; 4 | 5 | export const Step1: Component = () => { 6 | const { formHandler } = useFormContext(); 7 | 8 | return ( 9 |
10 |
11 |

Personal info:

12 |
13 |
14 | 19 |
20 |
21 | 26 |
27 |
28 | 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /www/src/implementations/MultiStepForm/zod/Step1.tsx: -------------------------------------------------------------------------------- 1 | import { Radios, TextInput } from '@components'; 2 | import { Component } from 'solid-js'; 3 | import { useFormContext } from './context'; 4 | 5 | export const Step1: Component = () => { 6 | const { formHandler } = useFormContext(); 7 | 8 | return ( 9 |
10 |
11 |

Personal info:

12 |
13 |
14 | 19 |
20 |
21 | 26 |
27 |
28 | 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /www/src/code-snippets/ValidatingCheckboxes1.tsx: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | ( 7 | <> 8 | 9 | {(favoriteFood, i) => ( 10 |
16 | 27 | 30 |
31 | )} 32 |
33 | 34 |
{field.helpers.errorMessage}
35 |
36 | 37 | )} 38 | />; 39 | -------------------------------------------------------------------------------- /www/src/components/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { FieldProps, Field } from 'solid-form-handler'; 2 | import { Component, JSX, Show, splitProps } from 'solid-js'; 3 | 4 | export type TextInputProps = JSX.InputHTMLAttributes & 5 | FieldProps & { label?: string }; 6 | 7 | export const TextInput: Component = (props) => { 8 | const [local, rest] = splitProps(props, [ 9 | 'classList', 10 | 'label', 11 | 'formHandler', 12 | ]); 13 | 14 | return ( 15 | ( 19 |
20 | 21 | 24 | 25 | 33 | 34 |
{field.helpers.errorMessage}
35 |
36 |
37 | )} 38 | /> 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/SelectImpl/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { useFormHandler } from '@hooks'; 3 | import { ValidationSchema } from '@interfaces'; 4 | 5 | export const SelectImpl: Component<{ schema: ValidationSchema }> = (props) => { 6 | const formHandler = useFormHandler(props.schema); 7 | 8 | return ( 9 | <> 10 |
11 |

Select implementation

12 | 13 |
14 | 25 | {formHandler.isFieldInvalid('country') && ( 26 | <> 27 |
28 | {formHandler.getFieldError('country')} 29 | 30 | )} 31 |
32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FileInputImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { FileInputImpl } from '.'; 3 | import { ySchema, zSchema } from './schemas'; 4 | 5 | describe('Text input with yup use case', () => { 6 | let fileInput: HTMLButtonElement; 7 | 8 | beforeEach(() => { 9 | const dom = render(() => ); 10 | fileInput = dom.container.querySelector('[type="button"]')!; 11 | }); 12 | 13 | it('must render error message on blur when value is empty', async () => { 14 | fireEvent.blur(fileInput); 15 | await waitFor(() => { 16 | expect(screen.getByText('File is required')).toBeDefined(); 17 | }); 18 | }); 19 | }); 20 | 21 | describe('Text input with zod use case', () => { 22 | let fileInput: HTMLButtonElement; 23 | 24 | beforeEach(() => { 25 | const dom = render(() => ); 26 | fileInput = dom.container.querySelector('[type="button"]')!; 27 | }); 28 | 29 | it('must render error message on blur when value is empty', async () => { 30 | fireEvent.blur(fileInput); 31 | await waitFor(() => { 32 | expect(screen.getByText('File is required')).toBeDefined(); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /www/src/pages/Docs/Components/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { NavLink } from '@solidjs/router'; 3 | 4 | export const Components: Component = () => ( 5 | <> 6 |

Components

7 |

8 | For doing code forms more legible and shorter is recommended you abstract 9 | each validation logic into SolidJS components with the help of the{' '} 10 | form handler <Field /> component. The following 11 | definitions are the suggested examples to follow and can be adapted for 12 | your preferable CSS framework or UI library. 13 |

14 |

15 | Here we will define the most common UI form components as SolidJS{' '} 16 | components: 17 |

18 |
    19 |
  • 20 | TextInput 21 |
  • 22 |
  • 23 | Select 24 |
  • 25 |
  • 26 | Checkbox 27 |
  • 28 |
  • 29 | Checkboxes 30 |
  • 31 |
  • 32 | Radios 33 |
  • 34 |
35 | 36 | ); 37 | -------------------------------------------------------------------------------- /www/src/utils/loadSnippets/index.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'solid-js/store'; 2 | 3 | const [snippetsStore, setSnippetsStore] = createStore<{ 4 | snippets: { 5 | [key: string]: any; 6 | }; 7 | loading: boolean; 8 | }>({ snippets: {}, loading: true }); 9 | 10 | const implementations = import.meta.glob( 11 | '../../implementations/**/*.(ts|tsx)', 12 | { 13 | as: 'raw', 14 | } 15 | ); 16 | const schemas = import.meta.glob('../../schemas/*.ts', { as: 'raw' }); 17 | const apis = import.meta.glob('../../apis/**/*.ts', { as: 'raw' }); 18 | const codeSnippets = import.meta.glob('../../code-snippets/**/*.(ts|tsx)', { 19 | as: 'raw', 20 | }); 21 | const components = import.meta.glob('../../components/**/*.tsx', { as: 'raw' }); 22 | const modules = { 23 | ...implementations, 24 | ...schemas, 25 | ...apis, 26 | ...codeSnippets, 27 | ...components, 28 | }; 29 | 30 | export const loadSnippets = async () => { 31 | const promises: Promise[] = []; 32 | 33 | for (let [key, value] of Object.entries(modules)) { 34 | promises.push( 35 | new Promise(async (resolve) => { 36 | setSnippetsStore('snippets', key, await value()); 37 | resolve(); 38 | }) 39 | ); 40 | } 41 | 42 | await Promise.all(promises); 43 | setSnippetsStore('loading', false); 44 | }; 45 | 46 | export { snippetsStore }; 47 | -------------------------------------------------------------------------------- /www/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/dist/css/bootstrap.min.css'; 2 | @import 'bootstrap-icons/font/bootstrap-icons.css'; 3 | @import 'font-awesome/css/font-awesome.min.css'; 4 | 5 | @font-face { 6 | font-family: Gordita; 7 | src: url(assets/fonts/gordita-regular.woff); 8 | font-weight: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: Gordita; 13 | src: url(assets/fonts/gordita-bold.woff); 14 | font-weight: bolder; 15 | font-weight: bold; 16 | } 17 | 18 | @font-face { 19 | font-family: Gordita; 20 | src: url(assets/fonts/gordita-regular-italic.woff); 21 | font-style: italic; 22 | } 23 | 24 | :root { 25 | --bs-primary-rgb: 77, 133, 193; 26 | --sidebar-bg: #f3f4f6; 27 | --nav-link: 107, 114, 128; 28 | --navbar-height: 62px; 29 | } 30 | 31 | html, 32 | body { 33 | height: 100%; 34 | scroll-behavior: initial !important; 35 | } 36 | 37 | #root { 38 | font-family: Gordita; 39 | min-height: 100vh; 40 | } 41 | 42 | @media (min-width: 991px) and (max-width: 1200px) { 43 | .table-scrollable { 44 | overflow-x: auto; 45 | max-width: 680px; 46 | } 47 | 48 | .table-scrollable table { 49 | min-width: 680px; 50 | } 51 | } 52 | 53 | @media (max-width: 725px) { 54 | .table-scrollable { 55 | overflow-x: auto; 56 | max-width: 680px; 57 | } 58 | 59 | .table-scrollable table { 60 | min-width: 680px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/CheckboxImpl/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Show } from 'solid-js'; 2 | import { useFormHandler } from '@hooks'; 3 | import { yupSchema } from '@adapters'; 4 | import * as yup from 'yup'; 5 | import { Field } from '@components'; 6 | 7 | type Schema = { 8 | policy: boolean; 9 | }; 10 | 11 | const schema: yup.Schema = yup.object().shape({ 12 | policy: yup.boolean().required().oneOf([true], 'Field must be checked'), 13 | }); 14 | 15 | export const CheckboxImpl: Component = () => { 16 | const formHandler = useFormHandler(yupSchema(schema)); 17 | 18 | return ( 19 | <> 20 |
21 |

Checkbox implementation

22 | 23 | ( 28 | <> 29 |
30 | 31 | 32 |
33 | 34 | {field.helpers.errorMessage} 35 | 36 | 37 | )} 38 | >
39 |
40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/lib/src/utils/createStore/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from '@utils'; 2 | 3 | describe('createStore', () => { 4 | it('CASE-1', () => { 5 | const [store, setStore] = createStore({}); 6 | setStore('key1', 1); 7 | expect(store).toMatchObject({ key1: 1 }); 8 | }); 9 | 10 | it('CASE-2', () => { 11 | const [store, setStore] = createStore({}); 12 | setStore('key1.key2.key3', 1); 13 | expect(store).toMatchObject({ key1: { key2: { key3: 1 } } }); 14 | }); 15 | 16 | it('CASE-3', () => { 17 | const [store, setStore] = createStore({ data: [{ name: 'Laura' }] }); 18 | setStore('data.1', { name: 'John' }); 19 | expect(store).toMatchObject({ data: [{ name: 'Laura' }, { name: 'John' }] }); 20 | }); 21 | 22 | it('CASE-4', () => { 23 | const [store, setStore] = createStore({ data: [{ name: 'Laura' }] }); 24 | setStore('data.1.0.state', 'John'); 25 | expect(store).toMatchObject({ data: [{ name: 'Laura' }, [{ state: 'John' }]] }); 26 | }); 27 | 28 | it('CASE-5', () => { 29 | const [store, setStore] = createStore([]); 30 | setStore('0.name', 'John'); 31 | expect(store).toMatchObject([{ name: 'John' }]); 32 | }); 33 | 34 | it('CASE-6', () => { 35 | const [store, setStore] = createStore([]); 36 | setStore('0.person.phones.0', 1112211); 37 | expect(store).toMatchObject([{ person: { phones: [1112211] } }]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/lib/src/utils/objectPaths/index.test.ts: -------------------------------------------------------------------------------- 1 | import { objectPaths } from '@utils'; 2 | 3 | describe('objectPaths', () => { 4 | it('CASE-1', () => { 5 | const data = [1, 2, 3, 4]; 6 | expect(objectPaths(data)).toEqual(expect.arrayContaining(['0', '1', '2', '3'])); 7 | }); 8 | 9 | it('CASE-2', () => { 10 | const data = [[1], [2], [3], [4]]; 11 | expect(objectPaths(data)).toEqual(expect.arrayContaining(['0.0', '1.0', '2.0', '3.0'])); 12 | }); 13 | 14 | it('CASE-3', () => { 15 | const data = 'Hello World'; 16 | expect(objectPaths(data)).toMatchObject([]); 17 | }); 18 | 19 | it('CASE-4', () => { 20 | const data = { key1: { key11: { key111: '' } }, key2: { key21: '' } }; 21 | expect(objectPaths(data)).toEqual( 22 | expect.arrayContaining(['key1', 'key1.key11', 'key1.key11.key111', 'key2', 'key2.key21']) 23 | ); 24 | }); 25 | 26 | it('CASE-5', () => { 27 | const data = { key1: { key11: { key111: [{ name: '' }] } }, key2: { key21: [1, 2, 3] } }; 28 | expect(objectPaths(data)).toEqual( 29 | expect.arrayContaining([ 30 | 'key1', 31 | 'key1.key11', 32 | 'key1.key11.key111', 33 | 'key1.key11.key111.0', 34 | 'key1.key11.key111.0.name', 35 | 'key2', 36 | 'key2.key21', 37 | 'key2.key21.0', 38 | 'key2.key21.1', 39 | 'key2.key21.2', 40 | ]) 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isEmpty/index.test.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from '@utils'; 2 | 3 | describe('isEmpty', () => { 4 | it('CASE-1', () => { 5 | expect(isEmpty(undefined)).toBe(true); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(isEmpty('')).toBe(true); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(isEmpty({})).toBe(true); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(isEmpty([])).toBe(true); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect(isEmpty([])).toBe(true); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect(isEmpty([undefined, undefined])).toBe(true); 26 | }); 27 | 28 | it('CASE-7', () => { 29 | expect(isEmpty(['', ''])).toBe(true); 30 | }); 31 | 32 | it('CASE-8', () => { 33 | expect(isEmpty([{ name: '', age: '' }])).toBe(true); 34 | }); 35 | 36 | it('CASE-9', () => { 37 | expect(isEmpty({ name: '', age: '' })).toBe(true); 38 | }); 39 | 40 | it('CASE-10', () => { 41 | expect(isEmpty(0)).toBe(false); 42 | }); 43 | 44 | it('CASE-11', () => { 45 | expect(isEmpty(111)).toBe(false); 46 | }); 47 | 48 | it('CASE-12', () => { 49 | expect(isEmpty([{ name: 'Laura', age: '' }])).toBe(false); 50 | }); 51 | 52 | it('CASE-13', () => { 53 | expect(isEmpty({ name: 'Laura', age: '' })).toBe(false); 54 | }); 55 | 56 | it('CASE-14', () => { 57 | expect(isEmpty('Hello')).toBe(false); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /www/scripts/syncExampleComponents.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const prettier = require('prettier'); 4 | 5 | const run = async () => { 6 | let baseInitialPath = path.join(__dirname, '../../', 'packages/lib/examples'); 7 | let baseTargetPath = path.join(__dirname, '..', 'src'); 8 | 9 | await writeComponents( 10 | `${baseInitialPath}/components`, 11 | `${baseTargetPath}/components` 12 | ); 13 | 14 | await writeComponents( 15 | `${baseInitialPath}/components/suid`, 16 | `${baseTargetPath}/components/suid` 17 | ); 18 | }; 19 | 20 | const writeComponents = async (initialPath, targetPath) => { 21 | let prettierOptions = await prettier.resolveConfig( 22 | path.join(__dirname, '..', '.prettierrc') 23 | ); 24 | prettierOptions = { ...prettierOptions, parser: 'babel-ts' }; 25 | 26 | const components = [ 27 | 'Checkbox', 28 | 'Checkboxes', 29 | 'Radios', 30 | 'Select', 31 | 'TextInput', 32 | 'FileInput', 33 | ]; 34 | 35 | components.forEach((component) => { 36 | let code = fs.readFileSync( 37 | path.join(initialPath, component, 'index.tsx'), 38 | 'utf8' 39 | ); 40 | code = code.replace('@example-components', '@components'); 41 | 42 | fs.writeFileSync( 43 | path.join(targetPath, component, 'index.tsx'), 44 | prettier.format(code, prettierOptions) 45 | ); 46 | }); 47 | }; 48 | 49 | run(); 50 | -------------------------------------------------------------------------------- /www/src/pages/Docs/ValidatingRadios/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Code, Implementation, Tabs } from '@components'; 3 | import { getRaw } from '@utils'; 4 | import { YupRadiosForm, ZodRadiosForm } from '@implementations'; 5 | import { Link } from '@solidjs/router'; 6 | 7 | export const ValidatingRadios: Component = () => ( 8 | <> 9 |

Validating Radios

10 |

11 | For controlling radios you need to use the <Field />{' '} 12 | component in radio-group mode. 13 |

14 | 15 |

16 | You can check the full implementation in the code tab. For doing radio 17 | group validation reusable, this logic can be abstracted into a{' '} 18 | Radios.tsx component. 19 |

20 | 26 | 27 | 28 | ), 29 | }, 30 | { 31 | text: 'zod', 32 | children: ( 33 | 34 | 35 | 36 | ), 37 | }, 38 | ]} 39 | /> 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /packages/lib/src/utils/createStore/index.ts: -------------------------------------------------------------------------------- 1 | import { formatObjectPath, get, isNumber } from '@utils'; 2 | import { createStore as baseCreateStore } from 'solid-js/store'; 3 | 4 | export const createStore = (data: T): [T, (path: string, value: any) => void] => { 5 | const [store, setBaseStore] = baseCreateStore(data); 6 | 7 | const setStore = (path: string, value: any) => { 8 | const arrPath = formatObjectPath(path).split('.'); 9 | let builtPath = ''; 10 | 11 | if (arrPath.length === 0) return; 12 | 13 | for (let i = 0; i < arrPath.length; i++) { 14 | const dot = builtPath ? '.' : ''; 15 | const currentKey = arrPath[i]; 16 | const nextKey = arrPath[i + 1]; 17 | builtPath = `${builtPath}${dot}${currentKey}`; 18 | 19 | const currentValue = get(store, builtPath); 20 | const builtPathArr = builtPath.split('.') as []; 21 | let obj: any = undefined; 22 | 23 | if (isNumber(currentKey) && isNumber(nextKey)) obj = []; 24 | if (!isNumber(currentKey) && isNumber(nextKey)) obj = []; 25 | if (isNumber(currentKey) && !isNumber(nextKey)) obj = {}; 26 | if (!isNumber(currentKey) && !isNumber(nextKey)) obj = {}; 27 | if (currentValue === undefined && nextKey !== undefined) setBaseStore(...builtPathArr, obj); 28 | if (nextKey === undefined) setBaseStore(...builtPathArr, value); 29 | } 30 | }; 31 | 32 | return [store, setStore]; 33 | }; 34 | -------------------------------------------------------------------------------- /www/src/components/Implementation/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code, CodeTabs, Tabs } from '@components'; 2 | import { Component, JSXElement, mergeProps, Switch, Match } from 'solid-js'; 3 | import { CodeTab } from '@interfaces'; 4 | 5 | export interface ImplementationProps { 6 | code?: string; 7 | codeTabs?: CodeTab[]; 8 | children?: JSXElement; 9 | language?: string; 10 | class?: string; 11 | } 12 | 13 | export const Implementation: Component = (props) => { 14 | props = mergeProps({ language: 'tsx' }, props); 15 | 16 | return ( 17 | {props.children}, 23 | }, 24 | { 25 | text: 'Code', 26 | children: ( 27 | 28 | 29 | 34 | 35 | 36 | 41 | 42 | 43 | ), 44 | }, 45 | ]} 46 | /> 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/lib/src/utils/set/index.test.ts: -------------------------------------------------------------------------------- 1 | import { set } from '@utils'; 2 | 3 | describe('set', () => { 4 | it('CASE-1', () => { 5 | expect(set({}, 'name', 'Laura')).toMatchObject({ name: 'Laura' }); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(set({}, 'contacts', [])).toMatchObject({ contacts: [] }); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(set([{ name: 'John' }], '0.name', 'Laura')).toMatchObject([{ name: 'Laura' }]); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(set([{ names: ['John', 'Gabriela'] }], '0.names.1', 'Laura')).toMatchObject([{ names: ['John', 'Laura'] }]); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect(set({}, 'data.name', 'Laura')).toMatchObject({ data: { name: 'Laura' } }); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect(set([], '0.name', { isValid: false })).toMatchObject([{ name: { isValid: false } }]); 26 | }); 27 | 28 | it('CASE-7', () => { 29 | expect(set(undefined, '0.name', { isValid: false })).toMatchObject([{ name: { isValid: false } }]); 30 | }); 31 | 32 | it('CASE-8', () => { 33 | expect(set(undefined, 'name.0', { isValid: false })).toMatchObject({ name: [{ isValid: false }] }); 34 | }); 35 | 36 | it('CASE-9', () => { 37 | expect(set([{ name: 'John' }], '1.name', 'Laura')).toMatchObject([{ name: 'John' }, { name: 'Laura' }]); 38 | }); 39 | 40 | it('CASE-10', () => { 41 | expect(set({}, 'key1.state', ['Laura'])).toMatchObject({ key1: { state: ['Laura'] } }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isNumber/index.test.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from '@utils'; 2 | 3 | describe('isNumber', () => { 4 | it('CASE-1', () => { 5 | expect(isNumber(1)).toBe(true); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(isNumber('1')).toBe(true); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(isNumber(1.1)).toBe(true); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(isNumber('1.1')).toBe(true); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect(isNumber('')).toBe(false); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect(isNumber(null)).toBe(false); 26 | }); 27 | 28 | it('CASE-7', () => { 29 | expect(isNumber(undefined)).toBe(false); 30 | }); 31 | 32 | it('CASE-8', () => { 33 | expect(isNumber('1.1a')).toBe(false); 34 | }); 35 | 36 | it('CASE-9', () => { 37 | expect(isNumber(-1)).toBe(true); 38 | }); 39 | 40 | it('CASE-10', () => { 41 | expect(isNumber(-1.1)).toBe(true); 42 | }); 43 | 44 | it('CASE-11', () => { 45 | expect(isNumber('-1')).toBe(true); 46 | }); 47 | 48 | it('CASE-12', () => { 49 | expect(isNumber('-1.1')).toBe(true); 50 | }); 51 | 52 | it('CASE-13', () => { 53 | expect(isNumber(2e64)).toBe(true); 54 | }); 55 | 56 | it('CASE-14', () => { 57 | expect(isNumber('2e64')).toBe(true); 58 | }); 59 | 60 | it('CASE-15', () => { 61 | expect(isNumber('0')).toBe(true); 62 | }); 63 | 64 | it('CASE-16', () => { 65 | expect(isNumber(0)).toBe(true); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/lib/src/utils/isInteger/index.test.ts: -------------------------------------------------------------------------------- 1 | import { isInteger } from '@utils'; 2 | 3 | describe('isInteger', () => { 4 | it('CASE-1', () => { 5 | expect(isInteger(1)).toBe(true); 6 | }); 7 | 8 | it('CASE-2', () => { 9 | expect(isInteger('1')).toBe(true); 10 | }); 11 | 12 | it('CASE-3', () => { 13 | expect(isInteger(1.1)).toBe(false); 14 | }); 15 | 16 | it('CASE-4', () => { 17 | expect(isInteger('1.1')).toBe(false); 18 | }); 19 | 20 | it('CASE-5', () => { 21 | expect(isInteger('')).toBe(false); 22 | }); 23 | 24 | it('CASE-6', () => { 25 | expect(isInteger(null)).toBe(false); 26 | }); 27 | 28 | it('CASE-7', () => { 29 | expect(isInteger(undefined)).toBe(false); 30 | }); 31 | 32 | it('CASE-8', () => { 33 | expect(isInteger('1.1a')).toBe(false); 34 | }); 35 | 36 | it('CASE-9', () => { 37 | expect(isInteger(-1)).toBe(true); 38 | }); 39 | 40 | it('CASE-10', () => { 41 | expect(isInteger(-1.1)).toBe(false); 42 | }); 43 | 44 | it('CASE-11', () => { 45 | expect(isInteger('-1')).toBe(true); 46 | }); 47 | 48 | it('CASE-12', () => { 49 | expect(isInteger('-1.1')).toBe(false); 50 | }); 51 | 52 | it('CASE-13', () => { 53 | expect(isInteger(2e64)).toBe(false); 54 | }); 55 | 56 | it('CASE-14', () => { 57 | expect(isInteger('2e64')).toBe(false); 58 | }); 59 | 60 | it('CASE-15', () => { 61 | expect(isInteger('0')).toBe(true); 62 | }); 63 | 64 | it('CASE-16', () => { 65 | expect(isInteger(0)).toBe(true); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/lib/src/utils/flattenObject/index.test.ts: -------------------------------------------------------------------------------- 1 | import { flattenObject } from '@utils'; 2 | 3 | describe('flattenObject', () => { 4 | it('CASE-1', () => { 5 | const flattenedObject = flattenObject({ name: 'John', age: 28 }); 6 | expect(flattenedObject).toMatchObject({ 7 | name: 'John', 8 | age: 28, 9 | }); 10 | }); 11 | 12 | it('CASE-2', () => { 13 | const flattenedObject = flattenObject([ 14 | { name: 'John', age: 28 }, 15 | { name: 'Laura', age: 32 }, 16 | ]); 17 | 18 | expect(flattenedObject).toMatchObject({ 19 | '0.name': 'John', 20 | '0.age': 28, 21 | '1.name': 'Laura', 22 | '1.age': 32, 23 | }); 24 | }); 25 | 26 | it('CASE-3', () => { 27 | const flattenedObject = flattenObject({ 28 | name: 'John', 29 | contact: { email: 'john@test.com', phone: 7272232 }, 30 | guests: [ 31 | { name: 'Louise', age: 22 }, 32 | { name: 'Lara', age: 26 }, 33 | ], 34 | }); 35 | 36 | expect(flattenedObject).toMatchObject({ 37 | name: 'John', 38 | 'contact.email': 'john@test.com', 39 | 'contact.phone': 7272232, 40 | 'guests.0.name': 'Louise', 41 | 'guests.0.age': 22, 42 | 'guests.1.name': 'Lara', 43 | 'guests.1.age': 26, 44 | }); 45 | }); 46 | 47 | it('CASE-4', () => { 48 | const flattenedObject = flattenObject({ 49 | favoriteFoods: [], 50 | }); 51 | 52 | expect(flattenedObject).toMatchObject({ 53 | favoriteFoods: [], 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /www/src/components/Radio/index.tsx: -------------------------------------------------------------------------------- 1 | import { FieldProps, Field } from 'solid-form-handler'; 2 | import { Component, JSX, splitProps } from 'solid-js'; 3 | 4 | export type RadioProps = Omit< 5 | JSX.InputHTMLAttributes, 6 | 'type' | 'value' 7 | > & 8 | FieldProps & { 9 | label?: string; 10 | }; 11 | 12 | export const Radio: Component = (props) => { 13 | const [local, rest] = splitProps(props, [ 14 | 'label', 15 | 'classList', 16 | 'formHandler', 17 | ]); 18 | 19 | return ( 20 | ( 24 |
25 |
31 | 40 | {local.label && ( 41 | 44 | )} 45 |
46 | {field.helpers.error && ( 47 |
{field.helpers.errorMessage}
48 | )} 49 |
50 | )} 51 | /> 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/FileInputImpl/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { FileInput } from '@example-components'; 3 | import { useFormHandler } from '@hooks'; 4 | import { ValidationSchema } from '@interfaces'; 5 | 6 | export const FileInputImpl: Component<{ schema: ValidationSchema }> = (props) => { 7 | const formHandler = useFormHandler(props.schema); 8 | 9 | const submit = async (event: Event) => { 10 | event.preventDefault(); 11 | try { 12 | await formHandler.validateForm(); 13 | alert(JSON.stringify(formHandler.formData())); 14 | formHandler.resetForm(); 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | }; 19 | 20 | return ( 21 | <> 22 |
23 |

File input implementation

24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 |

32 | Form data: 33 |

34 |
35 |           {JSON.stringify(formHandler.formData(), null, 2)}
36 |         
37 |
38 |           {JSON.stringify(formHandler._.getFormState(), null, 2)}
39 |         
40 |
41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/lib/examples/implementations/NestedFieldImpl/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render, fireEvent, waitFor } from 'solid-testing-library'; 2 | import { NestedFieldImpl } from '.'; 3 | 4 | describe('Nested field implementation', () => { 5 | it('submit button is enabled after fill form', async () => { 6 | render(() => ); 7 | fireEvent.click(screen.getByTestId('fill')); 8 | await waitFor(() => { 9 | expect((screen.getByTestId('submit') as HTMLButtonElement).disabled).toBe(false); 10 | expect(screen.getByTestId('form-status').innerHTML).toBe('Valid'); 11 | }); 12 | }); 13 | 14 | it('submit button is disabled after submitting form', async () => { 15 | render(() => ); 16 | fireEvent.click(screen.getByTestId('fill')); 17 | fireEvent.click(screen.getByTestId('submit')); 18 | await waitFor(() => { 19 | expect((screen.getByTestId('submit') as HTMLButtonElement).disabled).toBe(true); 20 | expect(screen.getByTestId('form-status').innerHTML).toBe('Invalid'); 21 | }); 22 | }); 23 | 24 | it('nested contact field is invalid', async () => { 25 | render(() => ); 26 | fireEvent.click(screen.getByTestId('fill')); 27 | fireEvent.input(screen.getByTestId('contact.email'), { target: { value: '' } }); 28 | fireEvent.input(screen.getByTestId('contact.phone'), { target: { value: '' } }); 29 | await waitFor(() => { 30 | expect(screen.getByTestId('contact-status').innerHTML).toBe('Invalid'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /www/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------