├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── LICENSE.md ├── README.md ├── modular-forms.code-workspace ├── package.json ├── packages ├── preact │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── adapters │ │ │ ├── index.ts │ │ │ ├── valiField.ts │ │ │ ├── valiForm.ts │ │ │ ├── zodField.ts │ │ │ └── zodForm.ts │ │ ├── components │ │ │ ├── Field.tsx │ │ │ ├── FieldArray.tsx │ │ │ ├── Form.tsx │ │ │ └── index.ts │ │ ├── exceptions │ │ │ ├── FormError.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormStore.ts │ │ │ ├── useLifecycle.ts │ │ │ └── useLiveSignal.ts │ │ ├── index.ts │ │ ├── methods │ │ │ ├── clearError.ts │ │ │ ├── clearResponse.ts │ │ │ ├── focus.ts │ │ │ ├── getError.ts │ │ │ ├── getErrors.ts │ │ │ ├── getValue.ts │ │ │ ├── getValues.ts │ │ │ ├── hasField.ts │ │ │ ├── hasFieldArray.ts │ │ │ ├── index.ts │ │ │ ├── insert.ts │ │ │ ├── move.ts │ │ │ ├── remove.ts │ │ │ ├── replace.ts │ │ │ ├── reset.ts │ │ │ ├── setError.ts │ │ │ ├── setResponse.ts │ │ │ ├── setValue.ts │ │ │ ├── setValues.ts │ │ │ ├── submit.ts │ │ │ ├── swap.ts │ │ │ └── validate.ts │ │ ├── transformation │ │ │ ├── index.ts │ │ │ ├── toCustom.ts │ │ │ ├── toLowerCase.ts │ │ │ ├── toTrimmed.ts │ │ │ └── toUpperCase.ts │ │ ├── types │ │ │ ├── field.ts │ │ │ ├── fieldArray.ts │ │ │ ├── form.ts │ │ │ ├── index.ts │ │ │ ├── path.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── getElementInput.ts │ │ │ ├── getFieldAndArrayStores.ts │ │ │ ├── getFieldArrayNames.ts │ │ │ ├── getFieldArrayState.ts │ │ │ ├── getFieldArrayStore.ts │ │ │ ├── getFieldNames.ts │ │ │ ├── getFieldState.ts │ │ │ ├── getFieldStore.ts │ │ │ ├── getFilteredNames.ts │ │ │ ├── getOptions.ts │ │ │ ├── getPathIndex.ts │ │ │ ├── getPathValue.ts │ │ │ ├── getUniqueId.ts │ │ │ ├── handleFieldEvent.ts │ │ │ ├── index.ts │ │ │ ├── initializeFieldArrayStore.ts │ │ │ ├── initializeFieldStore.ts │ │ │ ├── isFieldDirty.ts │ │ │ ├── readSignal.ts │ │ │ ├── removeInvalidNames.ts │ │ │ ├── setErrorResponse.ts │ │ │ ├── setFieldArrayState.ts │ │ │ ├── setFieldArrayValue.ts │ │ │ ├── setFieldState.ts │ │ │ ├── sortArrayPathIndex.ts │ │ │ ├── updateFieldArrayDirty.ts │ │ │ ├── updateFieldDirty.ts │ │ │ ├── updateFormDirty.ts │ │ │ ├── updateFormInvalid.ts │ │ │ ├── updateFormState.ts │ │ │ └── validateIfRequired.ts │ │ └── validation │ │ │ ├── custom.ts │ │ │ ├── email.ts │ │ │ ├── index.ts │ │ │ ├── maxLength.ts │ │ │ ├── maxRange.ts │ │ │ ├── maxSize.ts │ │ │ ├── maxTotalSize.ts │ │ │ ├── mimeType.ts │ │ │ ├── minLength.ts │ │ │ ├── minRange.ts │ │ │ ├── minSize.ts │ │ │ ├── minTotalSize.ts │ │ │ ├── pattern.ts │ │ │ ├── required.ts │ │ │ ├── url.ts │ │ │ └── value.ts │ ├── tsconfig.json │ └── vite.config.ts ├── qwik │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── formAction$.ts │ │ │ └── index.ts │ │ ├── adapters │ │ │ ├── index.ts │ │ │ ├── valiField$.ts │ │ │ ├── valiForm$.ts │ │ │ ├── zodField$.ts │ │ │ └── zodForm$.ts │ │ ├── components │ │ │ ├── Field.tsx │ │ │ ├── FieldArray.tsx │ │ │ ├── Form.tsx │ │ │ ├── Lifecycle.tsx │ │ │ └── index.ts │ │ ├── exceptions │ │ │ ├── FormError.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useForm.ts │ │ │ └── useFormStore.ts │ │ ├── index.ts │ │ ├── methods │ │ │ ├── clearError.ts │ │ │ ├── clearResponse.ts │ │ │ ├── focus.ts │ │ │ ├── getError.ts │ │ │ ├── getErrors.ts │ │ │ ├── getValue.ts │ │ │ ├── getValues.ts │ │ │ ├── hasField.ts │ │ │ ├── hasFieldArray.ts │ │ │ ├── index.ts │ │ │ ├── insert.ts │ │ │ ├── move.ts │ │ │ ├── remove.ts │ │ │ ├── replace.ts │ │ │ ├── reset.ts │ │ │ ├── setError.ts │ │ │ ├── setResponse.ts │ │ │ ├── setValue.ts │ │ │ ├── setValues.ts │ │ │ ├── submit.ts │ │ │ ├── swap.ts │ │ │ └── validate.ts │ │ ├── transformation │ │ │ ├── index.ts │ │ │ ├── toCustom$.ts │ │ │ ├── toLowerCase.ts │ │ │ ├── toTrimmed.ts │ │ │ └── toUpperCase.ts │ │ ├── types │ │ │ ├── field.ts │ │ │ ├── fieldArray.ts │ │ │ ├── form.ts │ │ │ ├── index.ts │ │ │ ├── path.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── getElementInput.ts │ │ │ ├── getFieldAndArrayStores.ts │ │ │ ├── getFieldArrayNames.ts │ │ │ ├── getFieldArrayState.ts │ │ │ ├── getFieldArrayStore.ts │ │ │ ├── getFieldNames.ts │ │ │ ├── getFieldState.ts │ │ │ ├── getFieldStore.ts │ │ │ ├── getFilteredNames.ts │ │ │ ├── getInitialFieldArrayStore.ts │ │ │ ├── getInitialFieldStore.ts │ │ │ ├── getInitialStores.ts │ │ │ ├── getOptions.ts │ │ │ ├── getParsedZodSchema.ts │ │ │ ├── getPathIndex.ts │ │ │ ├── getPathValue.ts │ │ │ ├── getUniqueId.ts │ │ │ ├── handleFieldEvent.ts │ │ │ ├── index.ts │ │ │ ├── initializeFieldArrayStore.ts │ │ │ ├── initializeFieldStore.ts │ │ │ ├── isFieldDirty.ts │ │ │ ├── removeInvalidNames.ts │ │ │ ├── setErrorResponse.ts │ │ │ ├── setFieldArrayState.ts │ │ │ ├── setFieldArrayValue.ts │ │ │ ├── setFieldErrors.ts │ │ │ ├── setFieldState.ts │ │ │ ├── sortArrayPathIndex.ts │ │ │ ├── updateFieldArrayDirty.ts │ │ │ ├── updateFieldDirty.ts │ │ │ ├── updateFormDirty.ts │ │ │ ├── updateFormInvalid.ts │ │ │ ├── updateFormState.ts │ │ │ └── validateIfRequired.ts │ │ └── validation │ │ │ ├── custom$.ts │ │ │ ├── email.ts │ │ │ ├── index.ts │ │ │ ├── maxLength.ts │ │ │ ├── maxRange.ts │ │ │ ├── maxSize.ts │ │ │ ├── maxTotalSize.ts │ │ │ ├── mimeType.ts │ │ │ ├── minLength.ts │ │ │ ├── minRange.ts │ │ │ ├── minSize.ts │ │ │ ├── minTotalSize.ts │ │ │ ├── pattern.ts │ │ │ ├── required.ts │ │ │ ├── url.ts │ │ │ └── value.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── adapters │ │ │ ├── index.ts │ │ │ ├── valiField.ts │ │ │ ├── valiForm.ts │ │ │ ├── zodField.ts │ │ │ └── zodForm.ts │ │ ├── components │ │ │ ├── Field.tsx │ │ │ ├── FieldArray.tsx │ │ │ ├── Form.tsx │ │ │ └── index.ts │ │ ├── exceptions │ │ │ ├── FormError.ts │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useForm.ts │ │ │ ├── useFormStore.ts │ │ │ ├── useLifecycle.ts │ │ │ └── useLiveSignal.ts │ │ ├── index.ts │ │ ├── methods │ │ │ ├── clearError.ts │ │ │ ├── clearResponse.ts │ │ │ ├── focus.ts │ │ │ ├── getError.ts │ │ │ ├── getErrors.ts │ │ │ ├── getValue.ts │ │ │ ├── getValues.ts │ │ │ ├── hasField.ts │ │ │ ├── hasFieldArray.ts │ │ │ ├── index.ts │ │ │ ├── insert.ts │ │ │ ├── move.ts │ │ │ ├── remove.ts │ │ │ ├── replace.ts │ │ │ ├── reset.ts │ │ │ ├── setError.ts │ │ │ ├── setResponse.ts │ │ │ ├── setValue.ts │ │ │ ├── setValues.ts │ │ │ ├── submit.ts │ │ │ ├── swap.ts │ │ │ └── validate.ts │ │ ├── transformation │ │ │ ├── index.ts │ │ │ ├── toCustom.ts │ │ │ ├── toLowerCase.ts │ │ │ ├── toTrimmed.ts │ │ │ └── toUpperCase.ts │ │ ├── types │ │ │ ├── field.ts │ │ │ ├── fieldArray.ts │ │ │ ├── form.ts │ │ │ ├── index.ts │ │ │ ├── path.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── getElementInput.ts │ │ │ ├── getFieldAndArrayStores.ts │ │ │ ├── getFieldArrayNames.ts │ │ │ ├── getFieldArrayState.ts │ │ │ ├── getFieldArrayStore.ts │ │ │ ├── getFieldNames.ts │ │ │ ├── getFieldState.ts │ │ │ ├── getFieldStore.ts │ │ │ ├── getFilteredNames.ts │ │ │ ├── getOptions.ts │ │ │ ├── getPathIndex.ts │ │ │ ├── getPathValue.ts │ │ │ ├── getUniqueId.ts │ │ │ ├── handleFieldEvent.ts │ │ │ ├── index.ts │ │ │ ├── initializeFieldArrayStore.ts │ │ │ ├── initializeFieldStore.ts │ │ │ ├── isFieldDirty.ts │ │ │ ├── readSignal.ts │ │ │ ├── removeInvalidNames.ts │ │ │ ├── setErrorResponse.ts │ │ │ ├── setFieldArrayState.ts │ │ │ ├── setFieldArrayValue.ts │ │ │ ├── setFieldState.ts │ │ │ ├── sortArrayPathIndex.ts │ │ │ ├── updateFieldArrayDirty.ts │ │ │ ├── updateFieldDirty.ts │ │ │ ├── updateFormDirty.ts │ │ │ ├── updateFormInvalid.ts │ │ │ ├── updateFormState.ts │ │ │ └── validateIfRequired.ts │ │ └── validation │ │ │ ├── custom.ts │ │ │ ├── email.ts │ │ │ ├── index.ts │ │ │ ├── maxLength.ts │ │ │ ├── maxRange.ts │ │ │ ├── maxSize.ts │ │ │ ├── maxTotalSize.ts │ │ │ ├── mimeType.ts │ │ │ ├── minLength.ts │ │ │ ├── minRange.ts │ │ │ ├── minSize.ts │ │ │ ├── minTotalSize.ts │ │ │ ├── pattern.ts │ │ │ ├── required.ts │ │ │ ├── url.ts │ │ │ └── value.ts │ ├── tsconfig.json │ └── vite.config.ts └── solid │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── adapters │ │ ├── index.ts │ │ ├── valiField.ts │ │ ├── valiForm.ts │ │ ├── zodField.ts │ │ └── zodForm.ts │ ├── components │ │ ├── Field.tsx │ │ ├── FieldArray.tsx │ │ ├── Form.tsx │ │ └── index.ts │ ├── exceptions │ │ ├── FormError.ts │ │ └── index.ts │ ├── index.ts │ ├── methods │ │ ├── clearError.ts │ │ ├── clearResponse.ts │ │ ├── focus.ts │ │ ├── getError.ts │ │ ├── getErrors.ts │ │ ├── getValue.ts │ │ ├── getValues.ts │ │ ├── hasField.ts │ │ ├── hasFieldArray.ts │ │ ├── index.ts │ │ ├── insert.ts │ │ ├── move.ts │ │ ├── remove.ts │ │ ├── replace.ts │ │ ├── reset.ts │ │ ├── setError.ts │ │ ├── setResponse.ts │ │ ├── setValue.ts │ │ ├── setValues.ts │ │ ├── submit.ts │ │ ├── swap.ts │ │ └── validate.ts │ ├── primitives │ │ ├── createForm.ts │ │ ├── createFormStore.ts │ │ ├── createLifecycle.ts │ │ ├── createSignal.ts │ │ └── index.ts │ ├── transformation │ │ ├── index.ts │ │ ├── toCustom.ts │ │ ├── toLowerCase.ts │ │ ├── toTrimmed.ts │ │ └── toUpperCase.ts │ ├── types │ │ ├── field.ts │ │ ├── fieldArray.ts │ │ ├── form.ts │ │ ├── index.ts │ │ ├── path.ts │ │ └── utils.ts │ ├── utils │ │ ├── getElementInput.ts │ │ ├── getFieldAndArrayStores.ts │ │ ├── getFieldArrayNames.ts │ │ ├── getFieldArrayState.ts │ │ ├── getFieldArrayStore.ts │ │ ├── getFieldNames.ts │ │ ├── getFieldState.ts │ │ ├── getFieldStore.ts │ │ ├── getFilteredNames.ts │ │ ├── getOptions.ts │ │ ├── getPathIndex.ts │ │ ├── getPathValue.ts │ │ ├── getUniqueId.ts │ │ ├── handleFieldEvent.ts │ │ ├── index.ts │ │ ├── initializeFieldArrayStore.ts │ │ ├── initializeFieldStore.ts │ │ ├── isFieldDirty.ts │ │ ├── removeInvalidNames.ts │ │ ├── setErrorResponse.ts │ │ ├── setFieldArrayState.ts │ │ ├── setFieldArrayValue.ts │ │ ├── setFieldState.ts │ │ ├── sortArrayPathIndex.ts │ │ ├── updateFieldArrayDirty.ts │ │ ├── updateFieldDirty.ts │ │ ├── updateFormDirty.ts │ │ ├── updateFormInvalid.ts │ │ ├── updateFormState.ts │ │ └── validateIfRequired.ts │ └── validation │ │ ├── custom.ts │ │ ├── email.ts │ │ ├── index.ts │ │ ├── maxLength.ts │ │ ├── maxRange.ts │ │ ├── maxSize.ts │ │ ├── maxTotalSize.ts │ │ ├── mimeType.ts │ │ ├── minLength.ts │ │ ├── minRange.ts │ │ ├── minSize.ts │ │ ├── minTotalSize.ts │ │ ├── pattern.ts │ │ ├── required.ts │ │ ├── url.ts │ │ └── value.ts │ └── tsconfig.json ├── playgrounds ├── preact │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── ActionButton.tsx │ │ │ ├── ButtonGroup.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ColorButton.tsx │ │ │ ├── Expandable.tsx │ │ │ ├── FileInput.tsx │ │ │ ├── FormFooter.tsx │ │ │ ├── FormHeader.tsx │ │ │ ├── InputError.tsx │ │ │ ├── InputLabel.tsx │ │ │ ├── Response.tsx │ │ │ ├── Select.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── UnstyledButton.tsx │ │ │ └── index.ts │ │ ├── global.css │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useEventListener.ts │ │ ├── icons │ │ │ ├── AngleDownIcon.tsx │ │ │ ├── NightIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── routes │ │ │ ├── login.tsx │ │ │ ├── nested.tsx │ │ │ ├── payment.tsx │ │ │ ├── special.tsx │ │ │ └── todos.tsx │ │ ├── utils │ │ │ ├── disableTransitions.ts │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── qwik │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .prettierignore │ ├── README.md │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ │ ├── favicon.ico │ │ └── manifest.json │ ├── src │ │ ├── components │ │ │ ├── ActionButton.tsx │ │ │ ├── ButtonGroup.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ColorButton.tsx │ │ │ ├── Expandable.tsx │ │ │ ├── FileInput.tsx │ │ │ ├── FormFooter.tsx │ │ │ ├── FormHeader.tsx │ │ │ ├── Head.tsx │ │ │ ├── InputError.tsx │ │ │ ├── InputLabel.tsx │ │ │ ├── Response.tsx │ │ │ ├── Select.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── UnstyledButton.tsx │ │ │ └── index.ts │ │ ├── entry.dev.tsx │ │ ├── entry.preview.tsx │ │ ├── entry.ssr.tsx │ │ ├── global.css │ │ ├── icons │ │ │ ├── AngleDownIcon.tsx │ │ │ └── index.ts │ │ ├── root.tsx │ │ ├── routes │ │ │ ├── (default) │ │ │ │ ├── index.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ │ └── index.tsx │ │ │ │ ├── nested │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment │ │ │ │ │ └── index.tsx │ │ │ │ ├── special │ │ │ │ │ └── index.tsx │ │ │ │ └── todos │ │ │ │ │ └── index.tsx │ │ │ ├── actions │ │ │ │ ├── index.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ │ └── index.tsx │ │ │ │ ├── special │ │ │ │ │ └── index.tsx │ │ │ │ └── todos │ │ │ │ │ └── index.tsx │ │ │ └── service-worker.ts │ │ └── utils │ │ │ ├── disableTransitions.ts │ │ │ └── index.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── react │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── components │ │ │ ├── ActionButton.tsx │ │ │ ├── ButtonGroup.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ColorButton.tsx │ │ │ ├── Expandable.tsx │ │ │ ├── FileInput.tsx │ │ │ ├── FormFooter.tsx │ │ │ ├── FormHeader.tsx │ │ │ ├── InputError.tsx │ │ │ ├── InputLabel.tsx │ │ │ ├── Response.tsx │ │ │ ├── Select.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Spinner.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── UnstyledButton.tsx │ │ │ └── index.ts │ │ ├── global.css │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useEventListener.ts │ │ ├── icons │ │ │ ├── AngleDownIcon.tsx │ │ │ ├── NightIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── routes │ │ │ ├── login.tsx │ │ │ ├── nested.tsx │ │ │ ├── payment.tsx │ │ │ ├── special.tsx │ │ │ └── todos.tsx │ │ ├── utils │ │ │ ├── disableTransitions.ts │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts └── solid │ ├── .gitignore │ ├── README.md │ ├── app.config.ts │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── app.tsx │ ├── components │ │ ├── ActionButton.tsx │ │ ├── ButtonGroup.tsx │ │ ├── Checkbox.tsx │ │ ├── ColorButton.tsx │ │ ├── Description.tsx │ │ ├── Expandable.tsx │ │ ├── FileInput.tsx │ │ ├── FormFooter.tsx │ │ ├── FormHeader.tsx │ │ ├── InputError.tsx │ │ ├── InputLabel.tsx │ │ ├── Select.tsx │ │ ├── Slider.tsx │ │ ├── Spinner.tsx │ │ ├── Tabs.tsx │ │ ├── TextInput.tsx │ │ ├── ThemeToggle.tsx │ │ ├── Title.tsx │ │ ├── UnstyledButton.tsx │ │ └── index.ts │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── global.d.ts │ ├── icons │ │ ├── AngleDownIcon.tsx │ │ ├── NightIcon.tsx │ │ ├── SunIcon.tsx │ │ └── index.ts │ ├── routes │ │ ├── [...404].tsx │ │ ├── index.tsx │ │ ├── login.tsx │ │ ├── nested.tsx │ │ ├── payment.tsx │ │ ├── special.tsx │ │ └── todos.tsx │ ├── styles.css │ └── utils │ │ ├── disableTransitions.ts │ │ ├── index.ts │ │ └── redirect.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.cjs └── website ├── .env ├── README.md ├── package.json ├── postcss.config.cjs ├── public ├── android-icon-192x192.png ├── android-icon-512x512.png ├── apple-icon-180x180.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── robots.txt ├── site.webmanifest └── sitemap.xml ├── src ├── components │ ├── ActionButton.tsx │ ├── ButtonGroup.tsx │ ├── Checkbox.tsx │ ├── ColorButton.tsx │ ├── Debugger.tsx │ ├── Description.tsx │ ├── DocsLayout.tsx │ ├── Expandable.tsx │ ├── FileInput.tsx │ ├── Footer.tsx │ ├── FormFooter.tsx │ ├── FormHeader.tsx │ ├── Framework.tsx │ ├── FrameworkPicker.tsx │ ├── GitHubIconLink.tsx │ ├── Hamburger.tsx │ ├── Head.tsx │ ├── Header.tsx │ ├── IconButton.tsx │ ├── InputError.tsx │ ├── InputLabel.tsx │ ├── Navigation.tsx │ ├── Property.tsx │ ├── RoutingIndicator.tsx │ ├── SearchToggle.tsx │ ├── Select.tsx │ ├── SideBar.tsx │ ├── Slider.tsx │ ├── Spinner.tsx │ ├── SystemIcon.tsx │ ├── Tabs.tsx │ ├── TextInput.tsx │ ├── TextLink.tsx │ ├── ThemeToggle.tsx │ ├── Title.tsx │ ├── UnstyledButton.tsx │ └── index.ts ├── contexts │ ├── form.tsx │ ├── framework.tsx │ ├── index.ts │ ├── search.tsx │ └── theme.tsx ├── cookies.ts ├── entry-client.tsx ├── entry-server.tsx ├── fonts │ ├── lexend-medium.woff2 │ └── lexend-regular.woff2 ├── icons │ ├── AngleDownIcon.tsx │ ├── AngleRightIcon.tsx │ ├── AngleUpIcon.tsx │ ├── ArrowLeftIcon.tsx │ ├── ArrowRrightIcon.tsx │ ├── CloseIcon.tsx │ ├── GitHubIcon.tsx │ ├── HashtagIcon.tsx │ ├── LogoIcon.tsx │ ├── NightIcon.tsx │ ├── PageIcon.tsx │ ├── PenIcon.tsx │ ├── PlusIcon.tsx │ ├── PreactIcon.tsx │ ├── QwikIcon.tsx │ ├── ReactIcon.tsx │ ├── SearchIcon.tsx │ ├── SolidIcon.tsx │ ├── SunIcon.tsx │ └── index.ts ├── images │ ├── blurred-code-dark.jpg │ ├── blurred-code-light.jpg │ └── index.ts ├── logos │ ├── PreactLogo.tsx │ ├── QwikLogo.tsx │ ├── ReactLogo.tsx │ ├── SolidLogo.tsx │ └── index.ts ├── primitives │ ├── createFocusTrap.ts │ └── index.ts ├── root.tsx ├── routes │ ├── (layout).tsx │ └── (layout) │ │ ├── [...404].tsx │ │ ├── [framework].tsx │ │ ├── [framework] │ │ ├── api.tsx │ │ ├── api │ │ │ ├── Field.mdx │ │ │ ├── FieldArray.mdx │ │ │ ├── FieldArrayStore.mdx │ │ │ ├── FieldElement.mdx │ │ │ ├── FieldElementProps.mdx │ │ │ ├── FieldEvent.mdx │ │ │ ├── FieldStore.mdx │ │ │ ├── FieldValue.mdx │ │ │ ├── FieldValues.mdx │ │ │ ├── Form.mdx │ │ │ ├── FormActionResult.mdx │ │ │ ├── FormActionStore.mdx │ │ │ ├── FormError.mdx │ │ │ ├── FormErrors.mdx │ │ │ ├── FormOptions.mdx │ │ │ ├── FormResponse.mdx │ │ │ ├── FormStore.mdx │ │ │ ├── InitialValues.mdx │ │ │ ├── Maybe.mdx │ │ │ ├── MaybeArray.mdx │ │ │ ├── MaybeFunction.mdx │ │ │ ├── MaybePromise.mdx │ │ │ ├── MaybeValue.mdx │ │ │ ├── PartialValues.mdx │ │ │ ├── ResponseData.mdx │ │ │ ├── SubmitHandler.mdx │ │ │ ├── TransformField.mdx │ │ │ ├── ValidateField.mdx │ │ │ ├── ValidateFieldArray.mdx │ │ │ ├── ValidateForm.mdx │ │ │ ├── clearError.mdx │ │ │ ├── clearResponse.mdx │ │ │ ├── createForm.mdx │ │ │ ├── createFormStore.mdx │ │ │ ├── custom$.mdx │ │ │ ├── custom.mdx │ │ │ ├── email.mdx │ │ │ ├── focus.mdx │ │ │ ├── formAction$.mdx │ │ │ ├── getError.mdx │ │ │ ├── getErrors.mdx │ │ │ ├── getValue.mdx │ │ │ ├── getValues.mdx │ │ │ ├── hasField.mdx │ │ │ ├── hasFieldArray.mdx │ │ │ ├── index.tsx │ │ │ ├── insert.mdx │ │ │ ├── maxLength.mdx │ │ │ ├── maxRange.mdx │ │ │ ├── maxSize.mdx │ │ │ ├── maxTotalSize.mdx │ │ │ ├── mimeType.mdx │ │ │ ├── minLength.mdx │ │ │ ├── minRange.mdx │ │ │ ├── minSize.mdx │ │ │ ├── minTotalSize.mdx │ │ │ ├── move.mdx │ │ │ ├── pattern.mdx │ │ │ ├── remove.mdx │ │ │ ├── replace.mdx │ │ │ ├── required.mdx │ │ │ ├── reset.mdx │ │ │ ├── setError.mdx │ │ │ ├── setResponse.mdx │ │ │ ├── setValue.mdx │ │ │ ├── setValues.mdx │ │ │ ├── submit.mdx │ │ │ ├── swap.mdx │ │ │ ├── toCustom$.mdx │ │ │ ├── toCustom.mdx │ │ │ ├── toLowerCase.mdx │ │ │ ├── toTrimmed.mdx │ │ │ ├── toUpperCase.mdx │ │ │ ├── url.mdx │ │ │ ├── useForm.mdx │ │ │ ├── useFormStore.mdx │ │ │ ├── valiField$.mdx │ │ │ ├── valiField.mdx │ │ │ ├── valiForm$.mdx │ │ │ ├── valiForm.mdx │ │ │ ├── validate.mdx │ │ │ ├── value.mdx │ │ │ ├── zodField$.mdx │ │ │ ├── zodField.mdx │ │ │ ├── zodForm$.mdx │ │ │ └── zodForm.mdx │ │ ├── guides.tsx │ │ ├── guides │ │ │ ├── add-fields-to-form.mdx │ │ │ ├── controlled-fields.mdx │ │ │ ├── create-your-form.mdx │ │ │ ├── define-your-form.mdx │ │ │ ├── enhanced-forms.mdx │ │ │ ├── faq.mdx │ │ │ ├── field-arrays.mdx │ │ │ ├── form-methods.mdx │ │ │ ├── handle-submission.mdx │ │ │ ├── index.tsx │ │ │ ├── input-components.mdx │ │ │ ├── installation.mdx │ │ │ ├── introduction.mdx │ │ │ ├── kobalte.mdx │ │ │ ├── nested-fields.mdx │ │ │ ├── philosophy.mdx │ │ │ ├── special-inputs.mdx │ │ │ ├── throw-form-errors.mdx │ │ │ ├── transform-inputs.mdx │ │ │ ├── typescript.mdx │ │ │ └── validate-your-fields.mdx │ │ ├── playground.tsx │ │ └── playground │ │ │ ├── index.tsx │ │ │ ├── login.tsx │ │ │ ├── nested.tsx │ │ │ ├── payment.tsx │ │ │ ├── special.tsx │ │ │ └── todos.tsx │ │ ├── api │ │ ├── [...all].tsx │ │ └── index.tsx │ │ ├── guides │ │ ├── [...all].tsx │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── legal.tsx │ │ ├── legal │ │ ├── contact.mdx │ │ └── privacy.mdx │ │ └── playground │ │ ├── [...all].tsx │ │ └── index.tsx ├── styles │ ├── base.css │ ├── components.css │ ├── fonts.css │ ├── pace.css │ └── utilities.css └── utils │ ├── disableTransitions.ts │ ├── getFrameworkName.ts │ ├── index.ts │ └── trackEvent.ts ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fabian-hiller] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Production 5 | dist 6 | build 7 | server 8 | 9 | # Backups 10 | backups 11 | 12 | # Services 13 | .vscode 14 | .solid 15 | .netlify 16 | netlify 17 | 18 | # Others 19 | stats.html 20 | logs 21 | *.log 22 | .cache 23 | temp 24 | 25 | # Local env files 26 | .env*.local 27 | 28 | # IDEs and editors 29 | .idea 30 | .project 31 | .classpath 32 | *.launch 33 | .settings 34 | 35 | # System files 36 | .DS_Store 37 | Thumbs.db 38 | *.pem 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /modular-forms.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [{ "path": "." }], 3 | "settings": { 4 | "editor.formatOnSave": true, 5 | "tailwindCSS.classAttributes": [ 6 | "class", 7 | "classList", 8 | "activeClass", 9 | "inactiveClass" 10 | ], 11 | "typescript.tsdk": "node_modules/typescript/lib" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modular-forms", 3 | "description": "The modular and type-safe form library for SolidJS and Qwik", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "author": "Fabian Hiller", 7 | "homepage": "https://modularforms.dev", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/fabian-hiller/modular-forms" 11 | }, 12 | "private": true, 13 | "devDependencies": { 14 | "prettier": "^3.3.3", 15 | "prettier-plugin-tailwindcss": "^0.6.6", 16 | "typescript": "5.5.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/preact/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['.eslintrc.cjs'], 4 | extends: [ 5 | 'eslint:recommended', 6 | 'preact', 7 | 'plugin:@typescript-eslint/recommended', 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | rules: { 12 | '@typescript-eslint/no-explicit-any': 'off', 13 | '@typescript-eslint/ban-ts-comment': 'off', 14 | '@typescript-eslint/consistent-type-imports': 'warn', 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | 'jest/no-deprecated-functions': 'off', 17 | 'no-duplicate-imports': 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/preact/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './valiField'; 2 | export * from './valiForm'; 3 | export * from './zodField'; 4 | export * from './zodForm'; 5 | -------------------------------------------------------------------------------- /packages/preact/src/adapters/valiField.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type GenericSchema, 3 | type GenericSchemaAsync, 4 | safeParseAsync, 5 | } from 'valibot'; 6 | import type { FieldValue, ValidateField, Maybe } from '../types'; 7 | 8 | /** 9 | * Creates a validation functions that parses the Valibot schema of a field. 10 | * 11 | * @param schema A Valibot schema. 12 | * 13 | * @returns A validation function. 14 | */ 15 | export function valiField( 16 | schema: GenericSchema | GenericSchemaAsync 17 | ): ValidateField { 18 | return async (value: Maybe) => { 19 | const result = await safeParseAsync(schema, value, { 20 | abortPipeEarly: true, 21 | }); 22 | return result.issues?.[0]?.message || ''; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/preact/src/adapters/zodField.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from 'zod'; 2 | import type { FieldValue, ValidateField, Maybe } from '../types'; 3 | 4 | /** 5 | * Creates a validation functions that parses the Zod schema of a field. 6 | * 7 | * @param schema A Zod schema. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function zodField( 12 | schema: ZodType 13 | ): ValidateField { 14 | return async (value: Maybe) => { 15 | const result = await schema.safeParseAsync(value); 16 | return result.success ? '' : result.error.issues[0].message; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/preact/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Field'; 2 | export * from './FieldArray'; 3 | export * from './Form'; 4 | -------------------------------------------------------------------------------- /packages/preact/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormError'; 2 | -------------------------------------------------------------------------------- /packages/preact/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useForm'; 2 | export * from './useFormStore'; 3 | export * from './useLifecycle'; 4 | export * from './useLiveSignal'; 5 | -------------------------------------------------------------------------------- /packages/preact/src/hooks/useLiveSignal.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlySignal, Signal } from '@preact/signals'; 2 | import { useSignal, useComputed } from '@preact/signals'; 3 | 4 | /** 5 | * Signal hook that updates when the reference of the value argument changes. 6 | * 7 | * @param value A signal that may change. 8 | * 9 | * @returns A readonly signal. 10 | */ 11 | export function useLiveSignal(value: Signal): ReadonlySignal { 12 | const signal = useSignal(value); 13 | if (signal.peek() !== value) signal.value = value; 14 | return useComputed(() => signal.value.value); 15 | } 16 | -------------------------------------------------------------------------------- /packages/preact/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters'; 2 | export * from './components'; 3 | export * from './exceptions'; 4 | export { useForm, useFormStore } from './hooks'; 5 | export * from './methods'; 6 | export * from './transformation'; 7 | export * from './types'; 8 | export * from './validation'; 9 | -------------------------------------------------------------------------------- /packages/preact/src/methods/clearError.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldValues, 3 | ResponseData, 4 | FormStore, 5 | FieldPath, 6 | FieldArrayPath, 7 | Maybe, 8 | } from '../types'; 9 | import { setError, type SetErrorOptions } from './setError'; 10 | 11 | /** 12 | * Clears the error of the specified field or field array. 13 | * 14 | * @param form The form of the field. 15 | * @param name The name of the field. 16 | * @param options The error options. 17 | */ 18 | export function clearError< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData 21 | >( 22 | form: FormStore, 23 | name: FieldPath | FieldArrayPath, 24 | options?: Maybe 25 | ): void { 26 | setError(form, name, '', options); 27 | } 28 | -------------------------------------------------------------------------------- /packages/preact/src/methods/clearResponse.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Clears the response of the form. 5 | * 6 | * @param form The form of the response. 7 | */ 8 | export function clearResponse< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.response.value = {}; 13 | } 14 | -------------------------------------------------------------------------------- /packages/preact/src/methods/focus.ts: -------------------------------------------------------------------------------- 1 | import type { FieldPath, FieldValues, FormStore, ResponseData } from '../types'; 2 | import { getFieldStore } from '../utils'; 3 | 4 | /** 5 | * Focuses the specified field of the form. 6 | * 7 | * @param form The form of the field. 8 | * @param name The name of the field. 9 | */ 10 | export function focus< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | name: FieldPath 16 | ): void { 17 | getFieldStore(form, name)?.elements.peek()[0]?.focus(); 18 | } 19 | -------------------------------------------------------------------------------- /packages/preact/src/methods/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clearError'; 2 | export * from './clearResponse'; 3 | export * from './focus'; 4 | export * from './getError'; 5 | export * from './getErrors'; 6 | export * from './getValue'; 7 | export * from './getValues'; 8 | export * from './hasField'; 9 | export * from './hasFieldArray'; 10 | export * from './insert'; 11 | export * from './move'; 12 | export * from './remove'; 13 | export * from './replace'; 14 | export * from './reset'; 15 | export * from './setError'; 16 | export * from './setResponse'; 17 | export * from './setValue'; 18 | export * from './setValues'; 19 | export * from './submit'; 20 | export * from './swap'; 21 | export * from './validate'; 22 | -------------------------------------------------------------------------------- /packages/preact/src/methods/submit.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Validates and submits the form. 5 | * 6 | * @param form The form to be submitted. 7 | */ 8 | export function submit< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.element.peek()?.requestSubmit(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/preact/src/transformation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toCustom'; 2 | export * from './toLowerCase'; 3 | export * from './toTrimmed'; 4 | export * from './toUpperCase'; 5 | -------------------------------------------------------------------------------- /packages/preact/src/transformation/toLowerCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to lowercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toLowerCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toLowerCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/transformation/toTrimmed.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that removes the leading and trailing 6 | * white space and line terminator characters from a string. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toTrimmed>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.trim() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/transformation/toUpperCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to uppercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toUpperCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toUpperCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field'; 2 | export * from './fieldArray'; 3 | export * from './form'; 4 | export * from './path'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /packages/preact/src/utils/getFieldArrayStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldArrayPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldArrayStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field array. 12 | * 13 | * @param form The form of the field array. 14 | * @param name The name of the field array. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldArrayStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldArrayName extends FieldArrayPath 22 | >( 23 | form: FormStore, 24 | name: TFieldArrayName 25 | ): Maybe { 26 | return form.internal.fieldArrays[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/preact/src/utils/getFieldStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field. 12 | * 13 | * @param form The form of the field. 14 | * @param name The name of the field. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldName extends FieldPath 22 | >( 23 | form: FormStore, 24 | name: TFieldName 25 | ): Maybe> { 26 | return form.internal.fields[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/preact/src/utils/getOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the options object from the arguments and returns it. 3 | * 4 | * @param arg1 Maybe the options object. 5 | * @param arg2 Maybe the options object. 6 | * 7 | * @returns The options object. 8 | */ 9 | export function getOptions< 10 | TName extends string, 11 | TOptions extends Record 12 | >(arg1?: TName | TName[] | TOptions, arg2?: TOptions): Partial { 13 | return (typeof arg1 !== 'string' && !Array.isArray(arg1) ? arg1 : arg2) || {}; 14 | } 15 | -------------------------------------------------------------------------------- /packages/preact/src/utils/getPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | 3 | /** 4 | * Returns the index of the path in the field array. 5 | * 6 | * @param name The name of the field array. 7 | * @param path The path to get the index from. 8 | * 9 | * @returns The field index in the array. 10 | */ 11 | export function getPathIndex( 12 | name: string, 13 | path: FieldPath | FieldArrayPath 14 | ): number { 15 | return +path.replace(`${name}.`, '').split('.')[0]; 16 | } 17 | -------------------------------------------------------------------------------- /packages/preact/src/utils/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | // Create counter variable 2 | let counter = 0; 3 | 4 | /** 5 | * Returns a unique ID counting up from zero. 6 | * 7 | * @returns A unique ID. 8 | */ 9 | export function getUniqueId(): number { 10 | return counter++; 11 | } 12 | -------------------------------------------------------------------------------- /packages/preact/src/utils/readSignal.ts: -------------------------------------------------------------------------------- 1 | import type { Signal } from '@preact/signals'; 2 | 3 | /** 4 | * It reads the value of a signal via .value or .peek(). 5 | * 6 | * @param signal The signal to be read. 7 | * @param peek Whether to subscribe. 8 | * 9 | * @returns The value of the signal. 10 | */ 11 | export function readSignal(signal: Signal, peek: boolean): T { 12 | return peek ? signal.peek() : signal.value; 13 | } 14 | -------------------------------------------------------------------------------- /packages/preact/src/utils/sortArrayPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | import { getPathIndex } from '../utils'; 3 | 4 | /** 5 | * Returns a function that sorts field names by their array path index. 6 | * 7 | * @param name The name of the field array. 8 | * 9 | * @returns The sort function. 10 | */ 11 | export function sortArrayPathIndex( 12 | name: FieldArrayPath 13 | ): ( 14 | pathA: FieldPath | FieldArrayPath, 15 | pathB: FieldPath | FieldArrayPath 16 | ) => number { 17 | return ( 18 | pathA: FieldPath | FieldArrayPath, 19 | pathB: FieldPath | FieldArrayPath 20 | ) => getPathIndex(name, pathA) - getPathIndex(name, pathB); 21 | } 22 | -------------------------------------------------------------------------------- /packages/preact/src/utils/updateFormDirty.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the dirty state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether dirty state is true. 9 | */ 10 | export function updateFormDirty< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >(form: FormStore, dirty?: Maybe): void { 14 | form.dirty.value = 15 | dirty || 16 | getFieldAndArrayStores(form).some( 17 | (fieldOrFieldArray) => 18 | fieldOrFieldArray.active.peek() && fieldOrFieldArray.dirty.peek() 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/preact/src/utils/updateFormInvalid.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the invalid state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether there is an error. 9 | */ 10 | export function updateFormInvalid< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | invalid?: Maybe 16 | ): void { 17 | form.invalid.value = 18 | invalid || 19 | getFieldAndArrayStores(form).some( 20 | (fieldOrFieldArray) => 21 | fieldOrFieldArray.active.peek() && fieldOrFieldArray.error.peek() 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/preact/src/validation/custom.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe, MaybePromise } from '../types'; 2 | 3 | /** 4 | * Creates a custom validation function. 5 | * 6 | * @param requirement The validation function. 7 | * @param error The error message. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function custom( 12 | requirement: (value: Maybe) => MaybePromise, 13 | error: string 14 | ): (value: Maybe) => Promise { 15 | return async (value: Maybe) => 16 | (Array.isArray(value) ? value.length : value || value === 0) && 17 | !(await requirement(value)) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/preact/src/validation/email.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a email. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function email(error: string): (value: Value) => string { 13 | return (value: Value) => 14 | value && 15 | !/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test( 16 | value 17 | ) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/preact/src/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './email'; 3 | export * from './maxLength'; 4 | export * from './maxRange'; 5 | export * from './maxSize'; 6 | export * from './maxTotalSize'; 7 | export * from './mimeType'; 8 | export * from './minLength'; 9 | export * from './minRange'; 10 | export * from './minSize'; 11 | export * from './minTotalSize'; 12 | export * from './pattern'; 13 | export * from './required'; 14 | export * from './url'; 15 | export * from './value'; 16 | -------------------------------------------------------------------------------- /packages/preact/src/validation/maxLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The maximum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/validation/maxRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The maximum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value! > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/validation/maxSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size > requirement) 21 | : value.size > requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/preact/src/validation/maxTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) > requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/preact/src/validation/mimeType.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file type. 7 | * 8 | * @param requirement The MIME types. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function mimeType( 14 | requirement: string | string[], 15 | error: string 16 | ): (value: Value) => string { 17 | const mimeTypes = Array.isArray(requirement) ? requirement : [requirement]; 18 | return (value: Value) => 19 | value && 20 | (Array.isArray(value) 21 | ? [...value].some((file) => !mimeTypes.includes(file.type)) 22 | : !mimeTypes.includes(value.type)) 23 | ? error 24 | : ''; 25 | } 26 | -------------------------------------------------------------------------------- /packages/preact/src/validation/minLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The minimum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/validation/minRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The minimum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/validation/minSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size < requirement) 21 | : value.size < requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/preact/src/validation/minTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) < requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/preact/src/validation/pattern.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the pattern of a string. 7 | * 8 | * @param requirement The regex pattern. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function pattern( 14 | requirement: RegExp, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => (value && !requirement.test(value) ? error : ''); 18 | } 19 | -------------------------------------------------------------------------------- /packages/preact/src/validation/required.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe } from '../types'; 2 | 3 | type Value = Maybe | number[]; 4 | 5 | /** 6 | * Creates a validation function that checks the existence of an input. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function required( 13 | error: string 14 | ): (value: Value) => string { 15 | return (value: Value) => 16 | (!value && value !== 0) || (Array.isArray(value) && !value.length) 17 | ? error 18 | : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/src/validation/url.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a URL. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function url(error: string): (value: Value) => string { 13 | return (value: Value) => { 14 | try { 15 | value && new URL(value); 16 | return ''; 17 | } catch (_) { 18 | return error; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/preact/src/validation/value.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation function that checks the value of an input for equality. 7 | * 8 | * @param requirement The value to be checked. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function value( 14 | requirement: string | number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value !== requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/preact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "preact", 11 | "outDir": "dist/source", 12 | "declaration": true, 13 | "declarationDir": "dist/types" 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/preact/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import preact from '@preact/preset-vite'; 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | target: 'es2020', 8 | outDir: 'dist', 9 | lib: { 10 | entry: './src/index.ts', 11 | formats: ['es', 'cjs'], 12 | fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`, 13 | }, 14 | minify: false, 15 | rollupOptions: { 16 | external: [ 17 | 'preact', 18 | 'preact/hooks', 19 | 'preact/jsx-runtime', 20 | '@preact/signals', 21 | 'valibot', 22 | 'zod', 23 | ], 24 | }, 25 | }, 26 | plugins: [preact()], 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /packages/qwik/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | .netlify 35 | pnpm-lock.yaml 36 | package-lock.json 37 | yarn.lock 38 | server 39 | -------------------------------------------------------------------------------- /packages/qwik/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './formAction$'; 2 | -------------------------------------------------------------------------------- /packages/qwik/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './valiField$'; 2 | export * from './valiForm$'; 3 | export * from './zodField$'; 4 | export * from './zodForm$'; 5 | -------------------------------------------------------------------------------- /packages/qwik/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Field'; 2 | export * from './FieldArray'; 3 | export * from './Form'; 4 | -------------------------------------------------------------------------------- /packages/qwik/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormError'; 2 | -------------------------------------------------------------------------------- /packages/qwik/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useForm'; 2 | export * from './useFormStore'; 3 | -------------------------------------------------------------------------------- /packages/qwik/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './adapters'; 3 | export * from './components'; 4 | export * from './exceptions'; 5 | export * from './hooks'; 6 | export * from './methods'; 7 | export * from './transformation'; 8 | export * from './types'; 9 | export * from './validation'; 10 | -------------------------------------------------------------------------------- /packages/qwik/src/methods/clearError.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldArrayPath, 3 | FieldPath, 4 | FieldValues, 5 | FormStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | import { type SetErrorOptions, setError } from './setError'; 10 | 11 | /** 12 | * Clears the error of the specified field or field array. 13 | * 14 | * @param form The form of the field. 15 | * @param name The name of the field. 16 | * @param options The error options. 17 | */ 18 | export function clearError< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData 21 | >( 22 | form: FormStore, 23 | name: FieldPath | FieldArrayPath, 24 | options?: Maybe 25 | ): void { 26 | setError(form, name, '', options); 27 | } 28 | -------------------------------------------------------------------------------- /packages/qwik/src/methods/clearResponse.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Clears the response of the form. 5 | * 6 | * @param form The form of the response. 7 | */ 8 | export function clearResponse< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.response = {}; 13 | } 14 | -------------------------------------------------------------------------------- /packages/qwik/src/methods/focus.ts: -------------------------------------------------------------------------------- 1 | import type { FieldPath, FieldValues, FormStore, ResponseData } from '../types'; 2 | import { getFieldStore } from '../utils'; 3 | 4 | /** 5 | * Focuses the specified field of the form. 6 | * 7 | * @param form The form of the field. 8 | * @param name The name of the field. 9 | */ 10 | export function focus< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | name: FieldPath 16 | ): void { 17 | getFieldStore(form, name)?.internal.elements[0]?.focus(); 18 | } 19 | -------------------------------------------------------------------------------- /packages/qwik/src/methods/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clearError'; 2 | export * from './clearResponse'; 3 | export * from './focus'; 4 | export * from './getError'; 5 | export * from './getErrors'; 6 | export * from './getValue'; 7 | export * from './getValues'; 8 | export * from './hasField'; 9 | export * from './hasFieldArray'; 10 | export * from './insert'; 11 | export * from './move'; 12 | export * from './remove'; 13 | export * from './replace'; 14 | export * from './reset'; 15 | export * from './setError'; 16 | export * from './setResponse'; 17 | export * from './setValue'; 18 | export * from './setValues'; 19 | export * from './submit'; 20 | export * from './swap'; 21 | export * from './validate'; 22 | -------------------------------------------------------------------------------- /packages/qwik/src/methods/submit.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Validates and submits the form. 5 | * 6 | * @param form The form to be submitted. 7 | */ 8 | export function submit< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.element?.requestSubmit(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/qwik/src/transformation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toCustom$'; 2 | export * from './toLowerCase'; 3 | export * from './toTrimmed'; 4 | export * from './toUpperCase'; 5 | -------------------------------------------------------------------------------- /packages/qwik/src/transformation/toLowerCase.ts: -------------------------------------------------------------------------------- 1 | import type { QRL } from '@builder.io/qwik'; 2 | import type { Maybe, MaybeValue, TransformField } from '../types'; 3 | import { toCustom$, type TransformOptions } from './toCustom$'; 4 | 5 | /** 6 | * Creates a transformation functions that converts all the alphabetic 7 | * characters in a string to lowercase. 8 | * 9 | * @param options The transform options. 10 | * 11 | * @returns A transformation functions. 12 | */ 13 | export function toLowerCase>( 14 | options: TransformOptions 15 | ): QRL> { 16 | return toCustom$( 17 | (value) => value && (value.toLowerCase() as Maybe), 18 | options 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/qwik/src/transformation/toTrimmed.ts: -------------------------------------------------------------------------------- 1 | import type { QRL } from '@builder.io/qwik'; 2 | import type { Maybe, MaybeValue, TransformField } from '../types'; 3 | import { toCustom$, type TransformOptions } from './toCustom$'; 4 | 5 | /** 6 | * Creates a transformation functions that removes the leading and trailing 7 | * white space and line terminator characters from a string. 8 | * 9 | * @param options The transform options. 10 | * 11 | * @returns A transformation functions. 12 | */ 13 | export function toTrimmed>( 14 | options: TransformOptions 15 | ): QRL> { 16 | return toCustom$( 17 | (value) => value && (value.trim() as Maybe), 18 | options 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/qwik/src/transformation/toUpperCase.ts: -------------------------------------------------------------------------------- 1 | import type { QRL } from '@builder.io/qwik'; 2 | import type { Maybe, MaybeValue, TransformField } from '../types'; 3 | import { toCustom$, type TransformOptions } from './toCustom$'; 4 | 5 | /** 6 | * Creates a transformation functions that converts all the alphabetic 7 | * characters in a string to uppercase. 8 | * 9 | * @param options The transform options. 10 | * 11 | * @returns A transformation functions. 12 | */ 13 | export function toUpperCase>( 14 | options: TransformOptions 15 | ): QRL> { 16 | return toCustom$( 17 | (value) => value && (value.toUpperCase() as Maybe), 18 | options 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/qwik/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field'; 2 | export * from './fieldArray'; 3 | export * from './form'; 4 | export * from './path'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getFieldArrayStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldArrayPath, 3 | FieldArrayStore, 4 | FieldValues, 5 | FormStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field array. 12 | * 13 | * @param form The form of the field array. 14 | * @param name The name of the field array. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldArrayStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldArrayName extends FieldArrayPath 22 | >( 23 | form: FormStore, 24 | name: TFieldArrayName 25 | ): Maybe> { 26 | return form.internal.fieldArrays[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getFieldStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldPath, 3 | FieldStore, 4 | FieldValues, 5 | FormStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field. 12 | * 13 | * @param form The form of the field. 14 | * @param name The name of the field. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldName extends FieldPath 22 | >( 23 | form: FormStore, 24 | name: TFieldName 25 | ): Maybe> { 26 | return form.internal.fields[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the options object from the arguments and returns it. 3 | * 4 | * @param arg1 Maybe the options object. 5 | * @param arg2 Maybe the options object. 6 | * 7 | * @returns The options object. 8 | */ 9 | export function getOptions< 10 | TName extends string, 11 | TOptions extends Record 12 | >(arg1?: TName | TName[] | TOptions, arg2?: TOptions): Partial { 13 | return (typeof arg1 !== 'string' && !Array.isArray(arg1) ? arg1 : arg2) || {}; 14 | } 15 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getParsedZodSchema.ts: -------------------------------------------------------------------------------- 1 | import type { QRL } from '@builder.io/qwik'; 2 | import type { SafeParseReturnType, ZodType } from 'zod'; 3 | import type { MaybeFunction } from '../types'; 4 | 5 | /** 6 | * Parses a value with a Zod schema and returns the result. 7 | * 8 | * @param schema The Zod schema. 9 | * @param value The value. 10 | * 11 | * @returns The parse result. 12 | */ 13 | export async function getParsedZodSchema( 14 | schema: QRL>>, 15 | value: Value 16 | ): Promise> { 17 | const zodSchema = await schema.resolve(); 18 | return ( 19 | typeof zodSchema === 'function' ? zodSchema() : zodSchema 20 | ).safeParseAsync(value); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | 3 | /** 4 | * Returns the index of the path in the field array. 5 | * 6 | * @param name The name of the field array. 7 | * @param path The path to get the index from. 8 | * 9 | * @returns The field index in the array. 10 | */ 11 | export function getPathIndex( 12 | name: string, 13 | path: FieldPath | FieldArrayPath 14 | ): number { 15 | return +path.replace(`${name}.`, '').split('.')[0]; 16 | } 17 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | // Create counter variable 2 | let counter = 0; 3 | 4 | /** 5 | * Returns a unique ID counting up from zero. 6 | * 7 | * @returns A unique ID. 8 | */ 9 | export function getUniqueId(): number { 10 | return counter++; 11 | } 12 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/sortArrayPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | import { getPathIndex } from '../utils'; 3 | 4 | /** 5 | * Returns a function that sorts field names by their array path index. 6 | * 7 | * @param name The name of the field array. 8 | * 9 | * @returns The sort function. 10 | */ 11 | export function sortArrayPathIndex( 12 | name: FieldArrayPath 13 | ): ( 14 | pathA: FieldPath | FieldArrayPath, 15 | pathB: FieldPath | FieldArrayPath 16 | ) => number { 17 | return ( 18 | pathA: FieldPath | FieldArrayPath, 19 | pathB: FieldPath | FieldArrayPath 20 | ) => getPathIndex(name, pathA) - getPathIndex(name, pathB); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/updateFormDirty.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the dirty state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether dirty state is true. 9 | */ 10 | export function updateFormDirty< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >(form: FormStore, dirty?: Maybe): void { 14 | form.dirty = 15 | dirty || 16 | getFieldAndArrayStores(form).some( 17 | (fieldOrFieldArray) => fieldOrFieldArray.active && fieldOrFieldArray.dirty 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/qwik/src/utils/updateFormInvalid.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the invalid state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether there is an error. 9 | */ 10 | export function updateFormInvalid< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | invalid?: Maybe 16 | ): void { 17 | form.invalid = 18 | invalid || 19 | getFieldAndArrayStores(form).some( 20 | (fieldOrFieldArray) => fieldOrFieldArray.active && fieldOrFieldArray.error 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/email.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation functions that validates a email. 8 | * 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function email(error: string): QRL<(value: Value) => string> { 14 | return $((value: Value) => 15 | value && 16 | !/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test( 17 | value 18 | ) 19 | ? error 20 | : '' 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom$'; 2 | export * from './email'; 3 | export * from './maxLength'; 4 | export * from './maxRange'; 5 | export * from './maxSize'; 6 | export * from './maxTotalSize'; 7 | export * from './mimeType'; 8 | export * from './minLength'; 9 | export * from './minRange'; 10 | export * from './minSize'; 11 | export * from './minTotalSize'; 12 | export * from './pattern'; 13 | export * from './required'; 14 | export * from './url'; 15 | export * from './value'; 16 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/maxLength.ts: -------------------------------------------------------------------------------- 1 | import { $, type NoSerialize, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue< 5 | string | string[] | number[] | NoSerialize[] | NoSerialize[] 6 | >; 7 | 8 | /** 9 | * Creates a validation functions that validates the length of a string or array. 10 | * 11 | * @param requirement The maximum string or array length. 12 | * @param error The error message. 13 | * 14 | * @returns A validation function. 15 | */ 16 | export function maxLength( 17 | requirement: number, 18 | error: string 19 | ): QRL<(value: Value) => string> { 20 | return $((value: Value) => 21 | value?.length && value.length > requirement ? error : '' 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/maxRange.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation functions that validates the range of a string, number or date. 8 | * 9 | * @param requirement The maximum range. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function maxRange( 15 | requirement: string | number | Date, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => 19 | (value || value === 0) && value! > requirement ? error : '' 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/maxTotalSize.ts: -------------------------------------------------------------------------------- 1 | import { $, type NoSerialize, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue[] | NoSerialize[]>; 5 | 6 | /** 7 | * Creates a validation functions that validates total file size of a file list. 8 | * 9 | * @param requirement The maximum size. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function maxTotalSize( 15 | requirement: number, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => 19 | value?.length && 20 | [...value].reduce((size, file) => size + file!.size, 0) > requirement 21 | ? error 22 | : '' 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/minLength.ts: -------------------------------------------------------------------------------- 1 | import { $, type NoSerialize, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue< 5 | string | string[] | number[] | NoSerialize[] | NoSerialize[] 6 | >; 7 | 8 | /** 9 | * Creates a validation functions that validates the length of a string or array. 10 | * 11 | * @param requirement The minimum string or array length. 12 | * @param error The error message. 13 | * 14 | * @returns A validation function. 15 | */ 16 | export function minLength( 17 | requirement: number, 18 | error: string 19 | ): QRL<(value: Value) => string> { 20 | return $((value: Value) => 21 | value?.length && value.length < requirement ? error : '' 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/minRange.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation functions that validates the range of a string, number or date. 8 | * 9 | * @param requirement The minimum range. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function minRange( 15 | requirement: string | number | Date, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => 19 | (value || value === 0) && value < requirement ? error : '' 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/minTotalSize.ts: -------------------------------------------------------------------------------- 1 | import { $, type NoSerialize, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue[] | NoSerialize[]>; 5 | 6 | /** 7 | * Creates a validation functions that validates total file size of a file list. 8 | * 9 | * @param requirement The minimum size. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function minTotalSize( 15 | requirement: number, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => 19 | value?.length && 20 | [...value].reduce((size, file) => size + file!.size, 0) < requirement 21 | ? error 22 | : '' 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/pattern.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation functions that validates the pattern of a string. 8 | * 9 | * @param requirement The regex pattern. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function pattern( 15 | requirement: RegExp, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => (value && !requirement.test(value) ? error : '')); 19 | } 20 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/required.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { FieldValue, Maybe } from '../types'; 3 | 4 | type Value = Maybe | number[]; 5 | 6 | /** 7 | * Creates a validation function that checks the existence of an input. 8 | * 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function required( 14 | error: string 15 | ): QRL<(value: Value) => string> { 16 | return $((value: Value) => 17 | (!value && value !== 0) || (Array.isArray(value) && !value.length) 18 | ? error 19 | : '' 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/url.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation functions that validates a URL. 8 | * 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function url(error: string): QRL<(value: Value) => string> { 14 | return $((value: Value) => { 15 | try { 16 | value && new URL(value); 17 | return ''; 18 | } catch (_) { 19 | return error; 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/qwik/src/validation/value.ts: -------------------------------------------------------------------------------- 1 | import { $, type QRL } from '@builder.io/qwik'; 2 | import type { MaybeValue } from '../types'; 3 | 4 | type Value = MaybeValue; 5 | 6 | /** 7 | * Creates a validation function that checks the value of an input for equality. 8 | * 9 | * @param requirement The value to be checked. 10 | * @param error The error message. 11 | * 12 | * @returns A validation function. 13 | */ 14 | export function value( 15 | requirement: string | number, 16 | error: string 17 | ): QRL<(value: Value) => string> { 18 | return $((value: Value) => 19 | (value || value === 0) && value !== requirement ? error : '' 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/qwik/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "@builder.io/qwik", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "skipLibCheck": true, 12 | "outDir": "dist/source", 13 | "declaration": true, 14 | "declarationDir": "dist/types" 15 | }, 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/react/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['.eslintrc.cjs'], 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react-hooks/recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | rules: { 12 | '@typescript-eslint/no-explicit-any': 'off', 13 | '@typescript-eslint/ban-ts-comment': 'off', 14 | '@typescript-eslint/consistent-type-imports': 'warn', 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | 'jest/no-deprecated-functions': 'off', 17 | 'no-duplicate-imports': 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './valiField'; 2 | export * from './valiForm'; 3 | export * from './zodField'; 4 | export * from './zodForm'; 5 | -------------------------------------------------------------------------------- /packages/react/src/adapters/valiField.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type GenericSchema, 3 | type GenericSchemaAsync, 4 | safeParseAsync, 5 | } from 'valibot'; 6 | import type { FieldValue, ValidateField, Maybe } from '../types'; 7 | 8 | /** 9 | * Creates a validation functions that parses the Valibot schema of a field. 10 | * 11 | * @param schema A Valibot schema. 12 | * 13 | * @returns A validation function. 14 | */ 15 | export function valiField( 16 | schema: GenericSchema | GenericSchemaAsync 17 | ): ValidateField { 18 | return async (value: Maybe) => { 19 | const result = await safeParseAsync(schema, value, { 20 | abortPipeEarly: true, 21 | }); 22 | return result.issues?.[0]?.message || ''; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/adapters/zodField.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from 'zod'; 2 | import type { FieldValue, ValidateField, Maybe } from '../types'; 3 | 4 | /** 5 | * Creates a validation functions that parses the Zod schema of a field. 6 | * 7 | * @param schema A Zod schema. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function zodField( 12 | schema: ZodType 13 | ): ValidateField { 14 | return async (value: Maybe) => { 15 | const result = await schema.safeParseAsync(value); 16 | return result.success ? '' : result.error.issues[0].message; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/react/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Field'; 2 | export * from './FieldArray'; 3 | export * from './Form'; 4 | -------------------------------------------------------------------------------- /packages/react/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormError'; 2 | -------------------------------------------------------------------------------- /packages/react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useForm'; 2 | export * from './useFormStore'; 3 | export * from './useLifecycle'; 4 | export * from './useLiveSignal'; 5 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useLiveSignal.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlySignal, Signal } from '@preact/signals-react'; 2 | import { useSignal, useComputed } from '@preact/signals-react'; 3 | 4 | /** 5 | * Signal hook that updates when the reference of the value argument changes. 6 | * 7 | * @param value A signal that may change. 8 | * 9 | * @returns A readonly signal. 10 | */ 11 | export function useLiveSignal(value: Signal): ReadonlySignal { 12 | const signal = useSignal(value); 13 | if (signal.peek() !== value) signal.value = value; 14 | return useComputed(() => signal.value.value); 15 | } 16 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters'; 2 | export * from './components'; 3 | export * from './exceptions'; 4 | export { useForm, useFormStore } from './hooks'; 5 | export * from './methods'; 6 | export * from './transformation'; 7 | export * from './types'; 8 | export * from './validation'; 9 | -------------------------------------------------------------------------------- /packages/react/src/methods/clearError.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldValues, 3 | ResponseData, 4 | FormStore, 5 | FieldPath, 6 | FieldArrayPath, 7 | Maybe, 8 | } from '../types'; 9 | import { setError, type SetErrorOptions } from './setError'; 10 | 11 | /** 12 | * Clears the error of the specified field or field array. 13 | * 14 | * @param form The form of the field. 15 | * @param name The name of the field. 16 | * @param options The error options. 17 | */ 18 | export function clearError< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData 21 | >( 22 | form: FormStore, 23 | name: FieldPath | FieldArrayPath, 24 | options?: Maybe 25 | ): void { 26 | setError(form, name, '', options); 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/src/methods/clearResponse.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Clears the response of the form. 5 | * 6 | * @param form The form of the response. 7 | */ 8 | export function clearResponse< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.response.value = {}; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/methods/focus.ts: -------------------------------------------------------------------------------- 1 | import type { FieldPath, FieldValues, FormStore, ResponseData } from '../types'; 2 | import { getFieldStore } from '../utils'; 3 | 4 | /** 5 | * Focuses the specified field of the form. 6 | * 7 | * @param form The form of the field. 8 | * @param name The name of the field. 9 | */ 10 | export function focus< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | name: FieldPath 16 | ): void { 17 | getFieldStore(form, name)?.elements.peek()[0]?.focus(); 18 | } 19 | -------------------------------------------------------------------------------- /packages/react/src/methods/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clearError'; 2 | export * from './clearResponse'; 3 | export * from './focus'; 4 | export * from './getError'; 5 | export * from './getErrors'; 6 | export * from './getValue'; 7 | export * from './getValues'; 8 | export * from './hasField'; 9 | export * from './hasFieldArray'; 10 | export * from './insert'; 11 | export * from './move'; 12 | export * from './remove'; 13 | export * from './replace'; 14 | export * from './reset'; 15 | export * from './setError'; 16 | export * from './setResponse'; 17 | export * from './setValue'; 18 | export * from './setValues'; 19 | export * from './submit'; 20 | export * from './swap'; 21 | export * from './validate'; 22 | -------------------------------------------------------------------------------- /packages/react/src/methods/submit.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Validates and submits the form. 5 | * 6 | * @param form The form to be submitted. 7 | */ 8 | export function submit< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.element.peek()?.requestSubmit(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/transformation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toCustom'; 2 | export * from './toLowerCase'; 3 | export * from './toTrimmed'; 4 | export * from './toUpperCase'; 5 | -------------------------------------------------------------------------------- /packages/react/src/transformation/toLowerCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to lowercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toLowerCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toLowerCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/transformation/toTrimmed.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that removes the leading and trailing 6 | * white space and line terminator characters from a string. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toTrimmed>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.trim() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/transformation/toUpperCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to uppercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toUpperCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toUpperCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field'; 2 | export * from './fieldArray'; 3 | export * from './form'; 4 | export * from './path'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /packages/react/src/utils/getFieldArrayStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldArrayPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldArrayStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field array. 12 | * 13 | * @param form The form of the field array. 14 | * @param name The name of the field array. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldArrayStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldArrayName extends FieldArrayPath 22 | >( 23 | form: FormStore, 24 | name: TFieldArrayName 25 | ): Maybe { 26 | return form.internal.fieldArrays[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/src/utils/getFieldStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field. 12 | * 13 | * @param form The form of the field. 14 | * @param name The name of the field. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldName extends FieldPath 22 | >( 23 | form: FormStore, 24 | name: TFieldName 25 | ): Maybe> { 26 | return form.internal.fields[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/src/utils/getOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the options object from the arguments and returns it. 3 | * 4 | * @param arg1 Maybe the options object. 5 | * @param arg2 Maybe the options object. 6 | * 7 | * @returns The options object. 8 | */ 9 | export function getOptions< 10 | TName extends string, 11 | TOptions extends Record 12 | >(arg1?: TName | TName[] | TOptions, arg2?: TOptions): Partial { 13 | return (typeof arg1 !== 'string' && !Array.isArray(arg1) ? arg1 : arg2) || {}; 14 | } 15 | -------------------------------------------------------------------------------- /packages/react/src/utils/getPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | 3 | /** 4 | * Returns the index of the path in the field array. 5 | * 6 | * @param name The name of the field array. 7 | * @param path The path to get the index from. 8 | * 9 | * @returns The field index in the array. 10 | */ 11 | export function getPathIndex( 12 | name: string, 13 | path: FieldPath | FieldArrayPath 14 | ): number { 15 | return +path.replace(`${name}.`, '').split('.')[0]; 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/src/utils/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | // Create counter variable 2 | let counter = 0; 3 | 4 | /** 5 | * Returns a unique ID counting up from zero. 6 | * 7 | * @returns A unique ID. 8 | */ 9 | export function getUniqueId(): number { 10 | return counter++; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/src/utils/readSignal.ts: -------------------------------------------------------------------------------- 1 | import type { Signal } from '@preact/signals-react'; 2 | 3 | /** 4 | * It reads the value of a signal via .value or .peek(). 5 | * 6 | * @param signal The signal to be read. 7 | * @param peek Whether to subscribe. 8 | * 9 | * @returns The value of the signal. 10 | */ 11 | export function readSignal(signal: Signal, peek: boolean): T { 12 | return peek ? signal.peek() : signal.value; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/utils/sortArrayPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | import { getPathIndex } from '../utils'; 3 | 4 | /** 5 | * Returns a function that sorts field names by their array path index. 6 | * 7 | * @param name The name of the field array. 8 | * 9 | * @returns The sort function. 10 | */ 11 | export function sortArrayPathIndex( 12 | name: FieldArrayPath 13 | ): ( 14 | pathA: FieldPath | FieldArrayPath, 15 | pathB: FieldPath | FieldArrayPath 16 | ) => number { 17 | return ( 18 | pathA: FieldPath | FieldArrayPath, 19 | pathB: FieldPath | FieldArrayPath 20 | ) => getPathIndex(name, pathA) - getPathIndex(name, pathB); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/utils/updateFormDirty.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the dirty state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether dirty state is true. 9 | */ 10 | export function updateFormDirty< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >(form: FormStore, dirty?: Maybe): void { 14 | form.dirty.value = 15 | dirty || 16 | getFieldAndArrayStores(form).some( 17 | (fieldOrFieldArray) => 18 | fieldOrFieldArray.active.peek() && fieldOrFieldArray.dirty.peek() 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/utils/updateFormInvalid.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, Maybe, ResponseData } from '../types'; 2 | import { getFieldAndArrayStores } from './getFieldAndArrayStores'; 3 | 4 | /** 5 | * Updates the invalid state of the form. 6 | * 7 | * @param form The store of the form. 8 | * @param dirty Whether there is an error. 9 | */ 10 | export function updateFormInvalid< 11 | TFieldValues extends FieldValues, 12 | TResponseData extends ResponseData 13 | >( 14 | form: FormStore, 15 | invalid?: Maybe 16 | ): void { 17 | form.invalid.value = 18 | invalid || 19 | getFieldAndArrayStores(form).some( 20 | (fieldOrFieldArray) => 21 | fieldOrFieldArray.active.peek() && fieldOrFieldArray.error.peek() 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/src/validation/custom.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe, MaybePromise } from '../types'; 2 | 3 | /** 4 | * Creates a custom validation function. 5 | * 6 | * @param requirement The validation function. 7 | * @param error The error message. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function custom( 12 | requirement: (value: Maybe) => MaybePromise, 13 | error: string 14 | ): (value: Maybe) => Promise { 15 | return async (value: Maybe) => 16 | (Array.isArray(value) ? value.length : value || value === 0) && 17 | !(await requirement(value)) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/validation/email.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a email. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function email(error: string): (value: Value) => string { 13 | return (value: Value) => 14 | value && 15 | !/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test( 16 | value 17 | ) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './email'; 3 | export * from './maxLength'; 4 | export * from './maxRange'; 5 | export * from './maxSize'; 6 | export * from './maxTotalSize'; 7 | export * from './mimeType'; 8 | export * from './minLength'; 9 | export * from './minRange'; 10 | export * from './minSize'; 11 | export * from './minTotalSize'; 12 | export * from './pattern'; 13 | export * from './required'; 14 | export * from './url'; 15 | export * from './value'; 16 | -------------------------------------------------------------------------------- /packages/react/src/validation/maxLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The maximum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/validation/maxRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The maximum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value! > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/validation/maxSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size > requirement) 21 | : value.size > requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/validation/maxTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) > requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/src/validation/mimeType.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file type. 7 | * 8 | * @param requirement The MIME types. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function mimeType( 14 | requirement: string | string[], 15 | error: string 16 | ): (value: Value) => string { 17 | const mimeTypes = Array.isArray(requirement) ? requirement : [requirement]; 18 | return (value: Value) => 19 | value && 20 | (Array.isArray(value) 21 | ? [...value].some((file) => !mimeTypes.includes(file.type)) 22 | : !mimeTypes.includes(value.type)) 23 | ? error 24 | : ''; 25 | } 26 | -------------------------------------------------------------------------------- /packages/react/src/validation/minLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The minimum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/validation/minRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The minimum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/validation/minSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size < requirement) 21 | : value.size < requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/validation/minTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) < requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/src/validation/pattern.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the pattern of a string. 7 | * 8 | * @param requirement The regex pattern. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function pattern( 14 | requirement: RegExp, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => (value && !requirement.test(value) ? error : ''); 18 | } 19 | -------------------------------------------------------------------------------- /packages/react/src/validation/required.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe } from '../types'; 2 | 3 | type Value = Maybe | number[]; 4 | 5 | /** 6 | * Creates a validation function that checks the existence of an input. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function required( 13 | error: string 14 | ): (value: Value) => string { 15 | return (value: Value) => 16 | (!value && value !== 0) || (Array.isArray(value) && !value.length) 17 | ? error 18 | : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/validation/url.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a URL. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function url(error: string): (value: Value) => string { 13 | return (value: Value) => { 14 | try { 15 | value && new URL(value); 16 | return ''; 17 | } catch (_) { 18 | return error; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/validation/value.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation function that checks the value of an input for equality. 7 | * 8 | * @param requirement The value to be checked. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function value( 14 | requirement: string | number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value !== requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "react", 11 | "outDir": "dist/source", 12 | "declaration": true, 13 | "declarationDir": "dist/types" 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | target: 'es2020', 8 | outDir: 'dist', 9 | lib: { 10 | entry: './src/index.ts', 11 | formats: ['es', 'cjs'], 12 | fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`, 13 | }, 14 | minify: false, 15 | rollupOptions: { 16 | external: [ 17 | 'react', 18 | 'react/jsx-runtime', 19 | '@preact/signals-react', 20 | 'valibot', 21 | 'zod', 22 | ], 23 | }, 24 | }, 25 | plugins: [react()], 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /packages/solid/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['.eslintrc.cjs'], 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:solid/typescript', 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint', 'solid'], 11 | rules: { 12 | '@typescript-eslint/no-explicit-any': 'off', 13 | '@typescript-eslint/ban-ts-comment': 'off', 14 | '@typescript-eslint/consistent-type-imports': 'warn', 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/solid/rollup.config.js: -------------------------------------------------------------------------------- 1 | import withSolid from 'rollup-preset-solid'; 2 | 3 | export default withSolid({ 4 | input: 'src/index.ts', 5 | targets: ['esm', 'cjs'], 6 | external: ['valibot', 'zod'], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/solid/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './valiField'; 2 | export * from './valiForm'; 3 | export * from './zodField'; 4 | export * from './zodForm'; 5 | -------------------------------------------------------------------------------- /packages/solid/src/adapters/valiField.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type GenericSchema, 3 | type GenericSchemaAsync, 4 | safeParseAsync, 5 | } from 'valibot'; 6 | import type { FieldValue, ValidateField, Maybe } from '../types'; 7 | 8 | /** 9 | * Creates a validation functions that parses the Valibot schema of a field. 10 | * 11 | * @param schema A Valibot schema. 12 | * 13 | * @returns A validation function. 14 | */ 15 | export function valiField( 16 | schema: GenericSchema | GenericSchemaAsync 17 | ): ValidateField { 18 | return async (value: Maybe) => { 19 | const result = await safeParseAsync(schema, value, { 20 | abortPipeEarly: true, 21 | }); 22 | return result.issues?.[0]?.message || ''; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/solid/src/adapters/zodField.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from 'zod'; 2 | import type { FieldValue, ValidateField, Maybe } from '../types'; 3 | 4 | /** 5 | * Creates a validation functions that parses the Zod schema of a field. 6 | * 7 | * @param schema A Zod schema. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function zodField( 12 | schema: ZodType 13 | ): ValidateField { 14 | return async (value: Maybe) => { 15 | const result = await schema.safeParseAsync(value); 16 | return result.success ? '' : result.error.issues[0].message; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/solid/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Field'; 2 | export * from './FieldArray'; 3 | export * from './Form'; 4 | -------------------------------------------------------------------------------- /packages/solid/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormError'; 2 | -------------------------------------------------------------------------------- /packages/solid/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters'; 2 | export * from './components'; 3 | export * from './exceptions'; 4 | export * from './methods'; 5 | export { createForm, createFormStore } from './primitives'; 6 | export * from './transformation'; 7 | export * from './types'; 8 | export * from './validation'; 9 | -------------------------------------------------------------------------------- /packages/solid/src/methods/clearError.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldValues, 3 | ResponseData, 4 | FormStore, 5 | FieldPath, 6 | FieldArrayPath, 7 | Maybe, 8 | } from '../types'; 9 | import { setError, type SetErrorOptions } from './setError'; 10 | 11 | /** 12 | * Clears the error of the specified field or field array. 13 | * 14 | * @param form The form of the field. 15 | * @param name The name of the field. 16 | * @param options The error options. 17 | */ 18 | export function clearError< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData 21 | >( 22 | form: FormStore, 23 | name: FieldPath | FieldArrayPath, 24 | options?: Maybe 25 | ): void { 26 | setError(form, name, '', options); 27 | } 28 | -------------------------------------------------------------------------------- /packages/solid/src/methods/clearResponse.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValues, FormStore, ResponseData } from '../types'; 2 | 3 | /** 4 | * Clears the response of the form. 5 | * 6 | * @param form The form of the response. 7 | */ 8 | export function clearResponse< 9 | TFieldValues extends FieldValues, 10 | TResponseData extends ResponseData 11 | >(form: FormStore): void { 12 | form.internal.response.set({}); 13 | } 14 | -------------------------------------------------------------------------------- /packages/solid/src/methods/focus.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from 'solid-js'; 2 | import type { FieldPath, FieldValues, FormStore, ResponseData } from '../types'; 3 | import { getFieldStore } from '../utils'; 4 | 5 | /** 6 | * Focuses the specified field of the form. 7 | * 8 | * @param form The form of the field. 9 | * @param name The name of the field. 10 | */ 11 | export function focus< 12 | TFieldValues extends FieldValues, 13 | TResponseData extends ResponseData 14 | >( 15 | form: FormStore, 16 | name: FieldPath 17 | ): void { 18 | untrack(() => getFieldStore(form, name)?.elements.get()[0]?.focus()); 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/methods/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clearError'; 2 | export * from './clearResponse'; 3 | export * from './focus'; 4 | export * from './getError'; 5 | export * from './getErrors'; 6 | export * from './getValue'; 7 | export * from './getValues'; 8 | export * from './hasField'; 9 | export * from './hasFieldArray'; 10 | export * from './insert'; 11 | export * from './move'; 12 | export * from './remove'; 13 | export * from './replace'; 14 | export * from './reset'; 15 | export * from './setError'; 16 | export * from './setResponse'; 17 | export * from './setValue'; 18 | export * from './setValues'; 19 | export * from './submit'; 20 | export * from './swap'; 21 | export * from './validate'; 22 | -------------------------------------------------------------------------------- /packages/solid/src/methods/submit.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from 'solid-js'; 2 | import type { FieldValues, FormStore, ResponseData } from '../types'; 3 | 4 | /** 5 | * Validates and submits the form. 6 | * 7 | * @param form The form to be submitted. 8 | */ 9 | export function submit< 10 | TFieldValues extends FieldValues, 11 | TResponseData extends ResponseData 12 | >(form: FormStore): void { 13 | untrack(() => form.element)?.requestSubmit(); 14 | } 15 | -------------------------------------------------------------------------------- /packages/solid/src/primitives/createSignal.ts: -------------------------------------------------------------------------------- 1 | import type { Accessor, Setter } from 'solid-js'; 2 | import { createSignal as createSolidSignal } from 'solid-js'; 3 | 4 | /** 5 | * Value type of signal object. 6 | */ 7 | export type Signal = { get: Accessor; set: Setter }; 8 | 9 | /** 10 | * Creates a simple reactive state with a getter and setter. 11 | */ 12 | export function createSignal(): Signal; 13 | export function createSignal(value: T): Signal; 14 | export function createSignal(value?: T) { 15 | const [get, set] = createSolidSignal(value); 16 | return { get, set }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/solid/src/primitives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createForm'; 2 | export * from './createFormStore'; 3 | export * from './createLifecycle'; 4 | export * from './createSignal'; 5 | -------------------------------------------------------------------------------- /packages/solid/src/transformation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toCustom'; 2 | export * from './toLowerCase'; 3 | export * from './toTrimmed'; 4 | export * from './toUpperCase'; 5 | -------------------------------------------------------------------------------- /packages/solid/src/transformation/toLowerCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to lowercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toLowerCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toLowerCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/transformation/toTrimmed.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that removes the leading and trailing 6 | * white space and line terminator characters from a string. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toTrimmed>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.trim() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/transformation/toUpperCase.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe, MaybeValue, TransformField } from '../types'; 2 | import { toCustom, type TransformOptions } from './toCustom'; 3 | 4 | /** 5 | * Creates a transformation functions that converts all the alphabetic 6 | * characters in a string to uppercase. 7 | * 8 | * @param options The transform options. 9 | * 10 | * @returns A transformation functions. 11 | */ 12 | export function toUpperCase>( 13 | options: TransformOptions 14 | ): TransformField { 15 | return toCustom( 16 | (value) => value && (value.toUpperCase() as Maybe), 17 | options 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './field'; 2 | export * from './fieldArray'; 3 | export * from './form'; 4 | export * from './path'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /packages/solid/src/utils/getFieldArrayStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldArrayPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldArrayStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field array. 12 | * 13 | * @param form The form of the field array. 14 | * @param name The name of the field array. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldArrayStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldArrayName extends FieldArrayPath 22 | >( 23 | form: FormStore, 24 | name: TFieldArrayName 25 | ): Maybe { 26 | return form.internal.fieldArrays[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/solid/src/utils/getFieldStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FieldPath, 3 | FieldValues, 4 | FormStore, 5 | InternalFieldStore, 6 | Maybe, 7 | ResponseData, 8 | } from '../types'; 9 | 10 | /** 11 | * Returns the store of a field. 12 | * 13 | * @param form The form of the field. 14 | * @param name The name of the field. 15 | * 16 | * @returns The reactive store. 17 | */ 18 | export function getFieldStore< 19 | TFieldValues extends FieldValues, 20 | TResponseData extends ResponseData, 21 | TFieldName extends FieldPath 22 | >( 23 | form: FormStore, 24 | name: TFieldName 25 | ): Maybe> { 26 | return form.internal.fields[name]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/solid/src/utils/getOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the options object from the arguments and returns it. 3 | * 4 | * @param arg1 Maybe the options object. 5 | * @param arg2 Maybe the options object. 6 | * 7 | * @returns The options object. 8 | */ 9 | export function getOptions< 10 | TName extends string, 11 | TOptions extends Record 12 | >(arg1?: TName | TName[] | TOptions, arg2?: TOptions): Partial { 13 | return (typeof arg1 !== 'string' && !Array.isArray(arg1) ? arg1 : arg2) || {}; 14 | } 15 | -------------------------------------------------------------------------------- /packages/solid/src/utils/getPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | 3 | /** 4 | * Returns the index of the path in the field array. 5 | * 6 | * @param name The name of the field array. 7 | * @param path The path to get the index from. 8 | * 9 | * @returns The field index in the array. 10 | */ 11 | export function getPathIndex( 12 | name: string, 13 | path: FieldPath | FieldArrayPath 14 | ): number { 15 | return +path.replace(`${name}.`, '').split('.')[0]; 16 | } 17 | -------------------------------------------------------------------------------- /packages/solid/src/utils/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | // Create counter variable 2 | let counter = 0; 3 | 4 | /** 5 | * Returns a unique ID counting up from zero. 6 | * 7 | * @returns A unique ID. 8 | */ 9 | export function getUniqueId(): number { 10 | return counter++; 11 | } 12 | -------------------------------------------------------------------------------- /packages/solid/src/utils/sortArrayPathIndex.ts: -------------------------------------------------------------------------------- 1 | import type { FieldArrayPath, FieldPath, FieldValues } from '../types'; 2 | import { getPathIndex } from '../utils'; 3 | 4 | /** 5 | * Returns a function that sorts field names by their array path index. 6 | * 7 | * @param name The name of the field array. 8 | * 9 | * @returns The sort function. 10 | */ 11 | export function sortArrayPathIndex( 12 | name: FieldArrayPath 13 | ): ( 14 | pathA: FieldPath | FieldArrayPath, 15 | pathB: FieldPath | FieldArrayPath 16 | ) => number { 17 | return ( 18 | pathA: FieldPath | FieldArrayPath, 19 | pathB: FieldPath | FieldArrayPath 20 | ) => getPathIndex(name, pathA) - getPathIndex(name, pathB); 21 | } 22 | -------------------------------------------------------------------------------- /packages/solid/src/validation/custom.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe, MaybePromise } from '../types'; 2 | 3 | /** 4 | * Creates a custom validation function. 5 | * 6 | * @param requirement The validation function. 7 | * @param error The error message. 8 | * 9 | * @returns A validation function. 10 | */ 11 | export function custom( 12 | requirement: (value: Maybe) => MaybePromise, 13 | error: string 14 | ): (value: Maybe) => Promise { 15 | return async (value: Maybe) => 16 | (Array.isArray(value) ? value.length : value || value === 0) && 17 | !(await requirement(value)) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/solid/src/validation/email.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a email. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function email(error: string): (value: Value) => string { 13 | return (value: Value) => 14 | value && 15 | !/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test( 16 | value 17 | ) 18 | ? error 19 | : ''; 20 | } 21 | -------------------------------------------------------------------------------- /packages/solid/src/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './email'; 3 | export * from './maxLength'; 4 | export * from './maxRange'; 5 | export * from './maxSize'; 6 | export * from './maxTotalSize'; 7 | export * from './mimeType'; 8 | export * from './minLength'; 9 | export * from './minRange'; 10 | export * from './minSize'; 11 | export * from './minTotalSize'; 12 | export * from './pattern'; 13 | export * from './required'; 14 | export * from './url'; 15 | export * from './value'; 16 | -------------------------------------------------------------------------------- /packages/solid/src/validation/maxLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The maximum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/validation/maxRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The maximum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value! > requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/validation/maxSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size > requirement) 21 | : value.size > requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/solid/src/validation/maxTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The maximum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function maxTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) > requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/solid/src/validation/mimeType.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file type. 7 | * 8 | * @param requirement The MIME types. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function mimeType( 14 | requirement: string | string[], 15 | error: string 16 | ): (value: Value) => string { 17 | const mimeTypes = Array.isArray(requirement) ? requirement : [requirement]; 18 | return (value: Value) => 19 | value && 20 | (Array.isArray(value) 21 | ? [...value].some((file) => !mimeTypes.includes(file.type)) 22 | : !mimeTypes.includes(value.type)) 23 | ? error 24 | : ''; 25 | } 26 | -------------------------------------------------------------------------------- /packages/solid/src/validation/minLength.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the length of a string or array. 7 | * 8 | * @param requirement The minimum string or array length. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minLength( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && value.length < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/validation/minRange.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the range of a string, number or date. 7 | * 8 | * @param requirement The minimum range. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minRange( 14 | requirement: string | number | Date, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value < requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/validation/minSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the file size. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value && 19 | (Array.isArray(value) 20 | ? [...value].some((file) => file.size < requirement) 21 | : value.size < requirement) 22 | ? error 23 | : ''; 24 | } 25 | -------------------------------------------------------------------------------- /packages/solid/src/validation/minTotalSize.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates total file size of a file list. 7 | * 8 | * @param requirement The minimum size. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function minTotalSize( 14 | requirement: number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | value?.length && 19 | [...value].reduce((size, file) => size + file.size, 0) < requirement 20 | ? error 21 | : ''; 22 | } 23 | -------------------------------------------------------------------------------- /packages/solid/src/validation/pattern.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates the pattern of a string. 7 | * 8 | * @param requirement The regex pattern. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function pattern( 14 | requirement: RegExp, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => (value && !requirement.test(value) ? error : ''); 18 | } 19 | -------------------------------------------------------------------------------- /packages/solid/src/validation/required.ts: -------------------------------------------------------------------------------- 1 | import type { FieldValue, Maybe } from '../types'; 2 | 3 | type Value = Maybe | number[]; 4 | 5 | /** 6 | * Creates a validation function that checks the existence of an input. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function required( 13 | error: string 14 | ): (value: Value) => string { 15 | return (value: Value) => 16 | (!value && value !== 0) || (Array.isArray(value) && !value.length) 17 | ? error 18 | : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/src/validation/url.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation functions that validates a URL. 7 | * 8 | * @param error The error message. 9 | * 10 | * @returns A validation function. 11 | */ 12 | export function url(error: string): (value: Value) => string { 13 | return (value: Value) => { 14 | try { 15 | value && new URL(value); 16 | return ''; 17 | } catch (_) { 18 | return error; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/solid/src/validation/value.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeValue } from '../types'; 2 | 3 | type Value = MaybeValue; 4 | 5 | /** 6 | * Creates a validation function that checks the value of an input for equality. 7 | * 8 | * @param requirement The value to be checked. 9 | * @param error The error message. 10 | * 11 | * @returns A validation function. 12 | */ 13 | export function value( 14 | requirement: string | number, 15 | error: string 16 | ): (value: Value) => string { 17 | return (value: Value) => 18 | (value || value === 0) && value !== requirement ? error : ''; 19 | } 20 | -------------------------------------------------------------------------------- /packages/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "jsx": "preserve", 10 | "jsxImportSource": "solid-js" 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /playgrounds/preact/README.md: -------------------------------------------------------------------------------- 1 | # Preact Playground 2 | 3 | The playground contains different forms to test the functionality of the library during development. 4 | 5 | ## Getting started 6 | 7 | Step 1: Clone repository 8 | 9 | ```bash 10 | git clone git@github.com:fabian-hiller/modular-forms.git 11 | ``` 12 | 13 | Step 2: Install dependencies 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | Step 3: Build `@modular-forms/preact` 20 | 21 | ```bash 22 | cd packages/preact && pnpm build 23 | ``` 24 | 25 | Step 4: Start development server 26 | 27 | ```bash 28 | cd ../../playgrounds/preact && pnpm dev 29 | ``` 30 | -------------------------------------------------------------------------------- /playgrounds/preact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Preact Playground | Modular Forms 8 | 13 | 14 | 18 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /playgrounds/preact/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /playgrounds/preact/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/playgrounds/preact/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/preact/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ComponentChildren } from 'preact'; 3 | 4 | type ButtonGroupProps = { 5 | children: ComponentChildren; 6 | class?: string; 7 | }; 8 | 9 | /** 10 | * Button group displays multiple related actions side-by-side and helps with 11 | * arrangement and spacing. 12 | */ 13 | export function ButtonGroup(props: ButtonGroupProps) { 14 | return ( 15 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /playgrounds/preact/src/components/FormFooter.tsx: -------------------------------------------------------------------------------- 1 | import { type FormStore, reset } from '@modular-forms/preact'; 2 | import { ActionButton } from './ActionButton'; 3 | 4 | type FormFooterProps = { 5 | of: FormStore; 6 | }; 7 | 8 | /** 9 | * Form footer with buttons to reset and submit the form. 10 | */ 11 | export function FormFooter({ of: form }: FormFooterProps) { 12 | return ( 13 |
14 | 15 | reset(form)} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/preact/src/components/InputError.tsx: -------------------------------------------------------------------------------- 1 | import { ReadonlySignal } from '@preact/signals'; 2 | import { Expandable } from './Expandable'; 3 | 4 | type InputErrorProps = { 5 | name: string; 6 | error?: ReadonlySignal; 7 | }; 8 | 9 | /** 10 | * Input error that tells the user what to do to fix the problem. 11 | */ 12 | export function InputError({ name, error }: InputErrorProps) { 13 | return ( 14 | 15 |
19 | {error} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/preact/src/components/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | type InputLabelProps = { 4 | name: string; 5 | label?: string; 6 | required?: boolean; 7 | margin?: 'none'; 8 | }; 9 | 10 | /** 11 | * Input label for a form field. 12 | */ 13 | export function InputLabel({ name, label, required, margin }: InputLabelProps) { 14 | return ( 15 | <> 16 | {label && ( 17 | 29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /playgrounds/preact/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Spinner provide a visual cue that an action is being processed. 3 | */ 4 | export function Spinner() { 5 | return ( 6 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /playgrounds/preact/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionButton'; 2 | export * from './ButtonGroup'; 3 | export * from './Checkbox'; 4 | export * from './ColorButton'; 5 | export * from './Expandable'; 6 | export * from './FileInput'; 7 | export * from './FormFooter'; 8 | export * from './FormHeader'; 9 | export * from './InputError'; 10 | export * from './InputLabel'; 11 | export * from './Response'; 12 | export * from './Select'; 13 | export * from './Slider'; 14 | export * from './Spinner'; 15 | export * from './Tabs'; 16 | export * from './TextInput'; 17 | export * from './UnstyledButton'; 18 | -------------------------------------------------------------------------------- /playgrounds/preact/src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Disable-transitions */ 6 | .disable-transitions, 7 | .disable-transitions * { 8 | transition: none !important; 9 | } 10 | 11 | /* Reset appearance */ 12 | [type='date'] { 13 | @apply flex appearance-none items-center justify-start; 14 | } 15 | -------------------------------------------------------------------------------- /playgrounds/preact/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useEventListener'; 2 | -------------------------------------------------------------------------------- /playgrounds/preact/src/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'preact/jsx-runtime'; 2 | 3 | export function AngleDownIcon(props: JSX.SVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/preact/src/icons/NightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'preact/jsx-runtime'; 2 | 3 | export function NightIcon(props: JSX.SVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/preact/src/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'preact/jsx-runtime'; 2 | 3 | export function SunIcon(props: JSX.SVGAttributes) { 4 | return ( 5 | 6 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /playgrounds/preact/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AngleDownIcon'; 2 | export * from './NightIcon'; 3 | export * from './SunIcon'; 4 | -------------------------------------------------------------------------------- /playgrounds/preact/src/utils/disableTransitions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Disables CSS transitions for a short moment. 3 | */ 4 | export function disableTransitions() { 5 | const { classList } = document.documentElement; 6 | classList.add('disable-transitions'); 7 | setTimeout(() => classList.remove('disable-transitions')); 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/preact/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disableTransitions'; 2 | -------------------------------------------------------------------------------- /playgrounds/preact/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playgrounds/preact/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{ts,tsx}'], 5 | darkMode: 'class', 6 | future: { 7 | hoverOnlyWhenSupported: true, 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | lexend: ['"Lexend"', ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /playgrounds/preact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "isolatedModules": true, 6 | "noEmit": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "target": "ESNext", 10 | "module": "ESNext", 11 | "moduleResolution": "node", 12 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 13 | "jsxImportSource": "preact", 14 | "jsx": "react-jsx", 15 | "types": ["vite/client"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playgrounds/preact/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import preact from '@preact/preset-vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [preact()], 6 | }); 7 | -------------------------------------------------------------------------------- /playgrounds/qwik/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | .netlify 35 | pnpm-lock.yaml 36 | package-lock.json 37 | yarn.lock 38 | server 39 | -------------------------------------------------------------------------------- /playgrounds/qwik/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | .netlify 35 | pnpm-lock.yaml 36 | package-lock.json 37 | yarn.lock 38 | server 39 | -------------------------------------------------------------------------------- /playgrounds/qwik/README.md: -------------------------------------------------------------------------------- 1 | # Qwik Playground 2 | 3 | The playground contains different forms to test the functionality of the library during development. 4 | 5 | ## Getting started 6 | 7 | Step 1: Clone repository 8 | 9 | ```bash 10 | git clone git@github.com:fabian-hiller/modular-forms.git 11 | ``` 12 | 13 | Step 2: Install dependencies 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | Step 3: Build `@modular-forms/qwik` 20 | 21 | ```bash 22 | cd packages/qwik && pnpm build 23 | ``` 24 | 25 | Step 4: Start development server 26 | 27 | ```bash 28 | cd ../../playgrounds/qwik && pnpm dev 29 | ``` 30 | -------------------------------------------------------------------------------- /playgrounds/qwik/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /playgrounds/qwik/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/playgrounds/qwik/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/qwik/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "@modular-forms/qwik-playground", 4 | "short_name": "Qwik Playground", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#fff", 8 | "description": "The Qwik playground of Modular Forms" 9 | } 10 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { component$, Slot } from '@builder.io/qwik'; 2 | import clsx from 'clsx'; 3 | 4 | type ButtonGroupProps = { 5 | class?: string; 6 | }; 7 | 8 | /** 9 | * Button group displays multiple related actions side-by-side and helps with 10 | * arrangement and spacing. 11 | */ 12 | export const ButtonGroup = component$((props: ButtonGroupProps) => ( 13 |
14 | 15 |
16 | )); 17 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from '@builder.io/qwik'; 2 | 3 | /** 4 | * Spinner provide a visual cue that an action is being processed. 5 | */ 6 | export const Spinner = component$(() => ( 7 |
11 | )); 12 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionButton'; 2 | export * from './ButtonGroup'; 3 | export * from './Checkbox'; 4 | export * from './ColorButton'; 5 | export * from './Expandable'; 6 | export * from './FileInput'; 7 | export * from './FormFooter'; 8 | export * from './FormHeader'; 9 | export * from './Head'; 10 | export * from './InputError'; 11 | export * from './InputLabel'; 12 | export * from './Response'; 13 | export * from './Select'; 14 | export * from './Slider'; 15 | export * from './Spinner'; 16 | export * from './Tabs'; 17 | export * from './TextInput'; 18 | export * from './UnstyledButton'; 19 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/entry.dev.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * Development entry point using only client-side modules: 5 | * - Do not use this mode in production! 6 | * - No SSR 7 | * - No portion of the application is pre-rendered on the server. 8 | * - All of the application is running eagerly in the browser. 9 | * - More code is transferred to the browser than in SSR mode. 10 | * - Optimizer/Serialization/Deserialization code is not exercised! 11 | */ 12 | import { render, type RenderOptions } from '@builder.io/qwik'; 13 | import Root from './root'; 14 | 15 | export default function (opts: RenderOptions) { 16 | return render(document, , opts); 17 | } 18 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/entry.preview.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * It's the bundle entry point for `npm run preview`. 5 | * That is, serving your app built in production mode. 6 | * 7 | * Feel free to modify this file, but don't remove it! 8 | * 9 | * Learn more about Vite's preview command: 10 | * - https://vitejs.dev/config/preview-options.html#preview-options 11 | * 12 | */ 13 | import { createQwikCity } from '@builder.io/qwik-city/middleware/node'; 14 | import qwikCityPlan from '@qwik-city-plan'; 15 | import render from './entry.ssr'; 16 | 17 | /** 18 | * The default export is the QwikCity adapter used by Vite preview. 19 | */ 20 | export default createQwikCity({ render, qwikCityPlan }); 21 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/entry.ssr.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * WHAT IS THIS FILE? 3 | * 4 | * SSR entry point, in all cases the application is render outside the browser, this 5 | * entry point will be the common one. 6 | * 7 | * - Server (express, cloudflare...) 8 | * - npm run start 9 | * - npm run preview 10 | * - npm run build 11 | * 12 | */ 13 | import { 14 | renderToStream, 15 | type RenderToStreamOptions, 16 | } from '@builder.io/qwik/server'; 17 | import { manifest } from '@qwik-client-manifest'; 18 | import Root from './root'; 19 | 20 | export default function (opts: RenderToStreamOptions) { 21 | return renderToStream(, { 22 | manifest, 23 | ...opts, 24 | // Use container attributes to set attributes on the html tag. 25 | containerAttributes: { 26 | lang: 'en-us', 27 | ...opts.containerAttributes, 28 | }, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Disable-transitions */ 6 | .disable-transitions, 7 | .disable-transitions * { 8 | transition: none !important; 9 | } 10 | 11 | /* Reset appearance */ 12 | [type='date'] { 13 | @apply flex appearance-none items-center justify-start; 14 | } 15 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { component$, type HTMLAttributes } from '@builder.io/qwik'; 2 | 3 | export const AngleDownIcon = component$( 4 | (props: HTMLAttributes) => ( 5 | 16 | 17 | 18 | ) 19 | ); 20 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AngleDownIcon'; 2 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/routes/(default)/index.tsx: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@builder.io/qwik-city'; 2 | 3 | export const onGet: RequestHandler = async ({ redirect }) => { 4 | throw redirect(301, '/login'); 5 | }; 6 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/routes/(default)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { component$, Slot } from '@builder.io/qwik'; 2 | import { Tabs } from '~/components'; 3 | 4 | export default component$(() => ( 5 | <> 6 | 7 |
8 | 9 |
10 | 11 | )); 12 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/routes/actions/index.tsx: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@builder.io/qwik-city'; 2 | 3 | export const onGet: RequestHandler = async ({ redirect }) => { 4 | throw redirect(301, '/actions/login'); 5 | }; 6 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/routes/actions/layout.tsx: -------------------------------------------------------------------------------- 1 | import { component$, Slot } from '@builder.io/qwik'; 2 | import { Tabs } from '~/components'; 3 | 4 | export default component$(() => ( 5 | <> 6 | 7 |
8 | 9 |
10 | 11 | )); 12 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/routes/service-worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * The service-worker.ts file is used to have state of the art prefetching. 5 | * https://qwik.builder.io/qwikcity/prefetching/overview/ 6 | * 7 | * Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline. 8 | * You can also use this file to add more functionality that runs in the service worker. 9 | */ 10 | import { setupServiceWorker } from '@builder.io/qwik-city/service-worker'; 11 | 12 | setupServiceWorker(); 13 | 14 | addEventListener('install', () => self.skipWaiting()); 15 | 16 | addEventListener('activate', () => self.clients.claim()); 17 | 18 | declare const self: ServiceWorkerGlobalScope; 19 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/utils/disableTransitions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Disables CSS transitions for a short moment. 3 | */ 4 | export function disableTransitions() { 5 | const { classList } = document.documentElement; 6 | classList.add('disable-transitions'); 7 | setTimeout(() => classList.remove('disable-transitions')); 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/qwik/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disableTransitions'; 2 | -------------------------------------------------------------------------------- /playgrounds/qwik/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: ['./src/**/*.{ts,tsx}'], 5 | darkMode: 'class', 6 | future: { 7 | hoverOnlyWhenSupported: true, 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | lexend: ['"Lexend"', ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /playgrounds/qwik/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { qwikVite } from '@builder.io/qwik/optimizer'; 3 | import { qwikCity } from '@builder.io/qwik-city/vite'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | 6 | export default defineConfig(() => { 7 | return { 8 | plugins: [qwikCity(), qwikVite(), tsconfigPaths()], 9 | preview: { 10 | headers: { 11 | 'Cache-Control': 'public, max-age=600', 12 | }, 13 | }, 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /playgrounds/react/README.md: -------------------------------------------------------------------------------- 1 | # React Playground 2 | 3 | The playground contains different forms to test the functionality of the library during development. 4 | 5 | ## Getting started 6 | 7 | Step 1: Clone repository 8 | 9 | ```bash 10 | git clone git@github.com:fabian-hiller/modular-forms.git 11 | ``` 12 | 13 | Step 2: Install dependencies 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | Step 3: Build `@modular-forms/react` 20 | 21 | ```bash 22 | cd packages/react && pnpm build 23 | ``` 24 | 25 | Step 4: Start development server 26 | 27 | ```bash 28 | cd ../../playgrounds/react && pnpm dev 29 | ``` 30 | -------------------------------------------------------------------------------- /playgrounds/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Playground | Modular Forms 8 | 13 | 14 | 18 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /playgrounds/react/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /playgrounds/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/playgrounds/react/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/react/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ReactNode } from 'react'; 3 | 4 | type ButtonGroupProps = { 5 | children: ReactNode; 6 | className?: string; 7 | }; 8 | 9 | /** 10 | * Button group displays multiple related actions side-by-side and helps with 11 | * arrangement and spacing. 12 | */ 13 | export function ButtonGroup({ className, ...props }: ButtonGroupProps) { 14 | return ( 15 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /playgrounds/react/src/components/FormFooter.tsx: -------------------------------------------------------------------------------- 1 | import { type FormStore, reset } from '@modular-forms/react'; 2 | import { ActionButton } from './ActionButton'; 3 | 4 | type FormFooterProps = { 5 | of: FormStore; 6 | }; 7 | 8 | /** 9 | * Form footer with buttons to reset and submit the form. 10 | */ 11 | export function FormFooter({ of: form }: FormFooterProps) { 12 | return ( 13 |
14 | 15 | reset(form)} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/react/src/components/InputError.tsx: -------------------------------------------------------------------------------- 1 | import { ReadonlySignal } from '@preact/signals-react'; 2 | import { Expandable } from './Expandable'; 3 | 4 | type InputErrorProps = { 5 | name: string; 6 | error?: ReadonlySignal; 7 | }; 8 | 9 | /** 10 | * Input error that tells the user what to do to fix the problem. 11 | */ 12 | export function InputError({ name, error }: InputErrorProps) { 13 | return ( 14 | 15 |
19 | {error} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/react/src/components/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | type InputLabelProps = { 4 | name: string; 5 | label?: string; 6 | required?: boolean; 7 | margin?: 'none'; 8 | }; 9 | 10 | /** 11 | * Input label for a form field. 12 | */ 13 | export function InputLabel({ name, label, required, margin }: InputLabelProps) { 14 | return ( 15 | <> 16 | {label && ( 17 | 29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /playgrounds/react/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Spinner provide a visual cue that an action is being processed. 3 | */ 4 | export function Spinner() { 5 | return ( 6 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /playgrounds/react/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionButton'; 2 | export * from './ButtonGroup'; 3 | export * from './Checkbox'; 4 | export * from './ColorButton'; 5 | export * from './Expandable'; 6 | export * from './FileInput'; 7 | export * from './FormFooter'; 8 | export * from './FormHeader'; 9 | export * from './InputError'; 10 | export * from './InputLabel'; 11 | export * from './Response'; 12 | export * from './Select'; 13 | export * from './Slider'; 14 | export * from './Spinner'; 15 | export * from './Tabs'; 16 | export * from './TextInput'; 17 | export * from './UnstyledButton'; 18 | -------------------------------------------------------------------------------- /playgrounds/react/src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Disable-transitions */ 6 | .disable-transitions, 7 | .disable-transitions * { 8 | transition: none !important; 9 | } 10 | 11 | /* Reset appearance */ 12 | [type='date'] { 13 | @apply flex appearance-none items-center justify-start; 14 | } 15 | -------------------------------------------------------------------------------- /playgrounds/react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useEventListener'; 2 | -------------------------------------------------------------------------------- /playgrounds/react/src/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from 'react'; 2 | 3 | export function AngleDownIcon(props: SVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/react/src/icons/NightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from 'react'; 2 | 3 | export function NightIcon(props: SVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/react/src/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGAttributes } from 'react'; 2 | 3 | export function SunIcon(props: SVGAttributes) { 4 | return ( 5 | 6 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /playgrounds/react/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AngleDownIcon'; 2 | export * from './NightIcon'; 3 | export * from './SunIcon'; 4 | -------------------------------------------------------------------------------- /playgrounds/react/src/utils/disableTransitions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Disables CSS transitions for a short moment. 3 | */ 4 | export function disableTransitions() { 5 | const { classList } = document.documentElement; 6 | classList.add('disable-transitions'); 7 | setTimeout(() => classList.remove('disable-transitions')); 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/react/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disableTransitions'; 2 | -------------------------------------------------------------------------------- /playgrounds/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playgrounds/react/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{ts,tsx}'], 5 | darkMode: 'class', 6 | future: { 7 | hoverOnlyWhenSupported: true, 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | lexend: ['"Lexend"', ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /playgrounds/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "skipLibCheck": true, 5 | "isolatedModules": true, 6 | "noEmit": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "target": "ESNext", 10 | "module": "ESNext", 11 | "moduleResolution": "node", 12 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 13 | "jsxImportSource": "react", 14 | "jsx": "react-jsx", 15 | "types": ["vite/client"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playgrounds/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /playgrounds/solid/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | .solid 4 | .output 5 | .vercel 6 | .netlify 7 | .vinxi 8 | 9 | # Environment 10 | .env 11 | .env*.local 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | *.launch 21 | .settings/ 22 | 23 | # Temp 24 | gitignore 25 | 26 | # System Files 27 | .DS_Store 28 | Thumbs.db 29 | -------------------------------------------------------------------------------- /playgrounds/solid/README.md: -------------------------------------------------------------------------------- 1 | # SolidJS Playground 2 | 3 | The playground contains different forms to test the functionality of the library during development. 4 | 5 | ## Getting started 6 | 7 | Step 1: Clone repository 8 | 9 | ```bash 10 | git clone git@github.com:fabian-hiller/modular-forms.git 11 | ``` 12 | 13 | Step 2: Install dependencies 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | Step 3: Build `@modular-forms/solid` 20 | 21 | ```bash 22 | cd packages/solid && pnpm build 23 | ``` 24 | 25 | Step 4: Start development server 26 | 27 | ```bash 28 | cd ../../playgrounds/solid && pnpm dev 29 | ``` 30 | -------------------------------------------------------------------------------- /playgrounds/solid/app.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@solidjs/start/config"; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /playgrounds/solid/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /playgrounds/solid/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/playgrounds/solid/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/solid/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { JSX } from 'solid-js'; 3 | 4 | type ButtonGroupProps = { 5 | class?: string; 6 | children: JSX.Element; 7 | }; 8 | 9 | /** 10 | * Button group displays multiple related actions side-by-side and helps with 11 | * arrangement and spacing. 12 | */ 13 | export function ButtonGroup(props: ButtonGroupProps) { 14 | return ( 15 |
18 | {props.children} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/Description.tsx: -------------------------------------------------------------------------------- 1 | import { Accessor } from 'solid-js'; 2 | import { Meta } from '@solidjs/meta'; 3 | 4 | type DescriptionProps = { 5 | children: string | Accessor; 6 | }; 7 | 8 | /** 9 | * Description of the page that is displayed in search engines and social media 10 | * snippets, for example. 11 | */ 12 | export function Description(props: DescriptionProps) { 13 | return ( 14 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/FormFooter.tsx: -------------------------------------------------------------------------------- 1 | import { FormStore, reset } from '@modular-forms/solid'; 2 | import { ActionButton } from './ActionButton'; 3 | 4 | type FormFooterProps = { 5 | of: FormStore; 6 | }; 7 | 8 | /** 9 | * Form footer with buttons to reset and submit the form. 10 | */ 11 | export function FormFooter(props: FormFooterProps) { 12 | return ( 13 |
14 | 15 | reset(props.of)} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/InputError.tsx: -------------------------------------------------------------------------------- 1 | import { Expandable } from './Expandable'; 2 | 3 | type InputErrorProps = { 4 | name: string; 5 | error?: string; 6 | }; 7 | 8 | /** 9 | * Input error that tells the user what to do to fix the problem. 10 | */ 11 | export function InputError(props: InputErrorProps) { 12 | return ( 13 | 14 |
18 | {props.error} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Show } from 'solid-js'; 3 | 4 | type InputLabelProps = { 5 | name: string; 6 | label?: string; 7 | required?: boolean; 8 | margin?: 'none'; 9 | }; 10 | 11 | /** 12 | * Input label for a form field. 13 | */ 14 | export function InputLabel(props: InputLabelProps) { 15 | return ( 16 | 17 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | type SpinnerProps = { 2 | label?: string; 3 | }; 4 | 5 | /** 6 | * Spinner provide a visual cue that an action is being processed. 7 | */ 8 | export function Spinner(props: SpinnerProps) { 9 | return ( 10 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '~/contexts'; 2 | import { NightIcon, SunIcon } from '~/icons'; 3 | 4 | /** 5 | * Button for switching the color theme. Depending on the status, a sun or 6 | * night icon is displayed. 7 | */ 8 | export function ThemeToggle() { 9 | const [getTheme, setTheme] = useTheme(); 10 | return ( 11 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import { Title as SolidTitle } from '@solidjs/meta'; 2 | import { useLocation } from '@solidjs/router'; 3 | 4 | type TitleProps = { 5 | children: string; 6 | }; 7 | 8 | /** 9 | * Title of the page that is displayed in the browser tab, for example. 10 | */ 11 | export function Title(props: TitleProps) { 12 | const location = useLocation(); 13 | return ( 14 | 15 | {props.children} 16 | {location.pathname !== '/' && ' | Modular Forms Playground'} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /playgrounds/solid/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionButton'; 2 | export * from './ButtonGroup'; 3 | export * from './Checkbox'; 4 | export * from './ColorButton'; 5 | export * from './Description'; 6 | export * from './FileInput'; 7 | export * from './FormFooter'; 8 | export * from './FormHeader'; 9 | export * from './InputError'; 10 | export * from './InputLabel'; 11 | export * from './Select'; 12 | export * from './Slider'; 13 | export * from './Tabs'; 14 | export * from './TextInput'; 15 | export * from './Title'; 16 | -------------------------------------------------------------------------------- /playgrounds/solid/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { mount, StartClient } from "@solidjs/start/client"; 3 | 4 | mount(() => , document.getElementById("app")!); 5 | -------------------------------------------------------------------------------- /playgrounds/solid/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playgrounds/solid/src/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function AngleDownIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/solid/src/icons/NightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function NightIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 6 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /playgrounds/solid/src/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function SunIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 6 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /playgrounds/solid/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AngleDownIcon'; 2 | export * from './NightIcon'; 3 | export * from './SunIcon'; 4 | -------------------------------------------------------------------------------- /playgrounds/solid/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from '@solidjs/router'; 2 | import { redirect } from '~/utils'; 3 | 4 | export function GET() { 5 | return redirect('login'); 6 | } 7 | 8 | export default function PlaygroundPage() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /playgrounds/solid/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Disable-transitions */ 6 | .disable-transitions, 7 | .disable-transitions * { 8 | transition: none !important; 9 | } 10 | 11 | /* Reset appearance */ 12 | [type='date'] { 13 | @apply flex appearance-none items-center justify-start; 14 | } 15 | -------------------------------------------------------------------------------- /playgrounds/solid/src/utils/disableTransitions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Disables CSS transitions for a short moment. 3 | */ 4 | export function disableTransitions() { 5 | const { classList } = document.documentElement; 6 | classList.add('disable-transitions'); 7 | setTimeout(() => classList.remove('disable-transitions')); 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/solid/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disableTransitions'; 2 | export * from './redirect'; 3 | -------------------------------------------------------------------------------- /playgrounds/solid/src/utils/redirect.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a redirects response to the client. 3 | * 4 | * @param href The href to redirect to. 5 | * 6 | * @returns A response object. 7 | */ 8 | export function redirect(href: string): Response { 9 | return new Response('', { 10 | status: 302, 11 | headers: { 12 | Location: href, 13 | }, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /playgrounds/solid/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import { fontFamily } from 'tailwindcss/defaultTheme'; 3 | export default { 4 | content: ['./src/**/*.{ts,tsx}'], 5 | darkMode: 'class', 6 | future: { 7 | hoverOnlyWhenSupported: true, 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | lexend: ['"Lexend"', ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /playgrounds/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "jsx": "preserve", 9 | "jsxImportSource": "solid-js", 10 | "allowJs": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "types": ["vinxi/types/client"], 14 | "isolatedModules": true, 15 | "paths": { 16 | "~/*": ["./src/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'playgrounds/*' 4 | - 'website' 5 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier-plugin-tailwindcss'], 3 | printWidth: 80, 4 | tabWidth: 2, 5 | useTabs: false, 6 | semi: true, 7 | singleQuote: true, 8 | quoteProps: 'as-needed', 9 | jsxSingleQuote: false, 10 | trailingComma: 'es5', 11 | bracketSpacing: true, 12 | bracketSameLine: false, 13 | arrowParens: 'always', 14 | endOfLine: 'lf', 15 | }; 16 | -------------------------------------------------------------------------------- /website/.env: -------------------------------------------------------------------------------- 1 | VITE_WEBSITE_URL="https://modularforms.dev" 2 | VITE_GITHUB_URL="https://github.com/fabian-hiller/modular-forms" 3 | VITE_GITHUB_WEBSITE_URL="https://github.com/fabian-hiller/modular-forms/blob/main/website" 4 | VITE_GITHUB_PLAYGROUNDS_URL="https://github.com/fabian-hiller/modular-forms/blob/main/playgrounds" 5 | VITE_STACKBLITZ_SOLID_URL="https://stackblitz.com/edit/modular-forms-solid" 6 | VITE_STACKBLITZ_QWIK_URL="https://stackblitz.com/edit/modular-forms-qwik" 7 | VITE_STACKBLITZ_QWIK_ACTIONS_URL="https://stackblitz.com/edit/modular-forms-qwik-actions" 8 | VITE_STACKBLITZ_PREACT_URL="https://stackblitz.com/edit/modular-forms-preact" 9 | VITE_STACKBLITZ_REACT_URL="https://stackblitz.com/edit/modular-forms-react" 10 | VITE_ALGOLIA_APP_ID="WPUKVFFXAK" 11 | VITE_ALGOLIA_INDEX_NAME="modularforms" 12 | VITE_ALGOLIA_PUBLIC_API_KEY="a1341ad26a8e6ef7890b254961746f93" -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | Our [website](https://modularforms.dev/) contains guides, an API reference and a playground where the state of the form can be monitored via the debugger. 4 | 5 | ## Getting started 6 | 7 | Step 1: Clone repository 8 | 9 | ```bash 10 | git clone git@github.com:fabian-hiller/modular-forms.git 11 | ``` 12 | 13 | Step 2: Install dependencies 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | Step 3: Build `@modular-forms/solid` 20 | 21 | ```bash 22 | cd packages/solid && pnpm build 23 | ``` 24 | 25 | Step 4: Start development server 26 | 27 | ```bash 28 | cd ../../website && pnpm dev 29 | ``` 30 | -------------------------------------------------------------------------------- /website/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/android-icon-192x192.png -------------------------------------------------------------------------------- /website/public/android-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/android-icon-512x512.png -------------------------------------------------------------------------------- /website/public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | 4 | Sitemap: https://www.example.com/sitemap.xml -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Modular Forms", 3 | "short_name": "Modular Forms", 4 | "description": "The modular and type safe form library", 5 | "theme_color": "#111827", 6 | "background_color": "#111827", 7 | "display": "standalone", 8 | "start_url": ".", 9 | "icons": [ 10 | { 11 | "src": "/android-icon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/android-icon-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /website/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { JSX } from 'solid-js'; 3 | 4 | type ButtonGroupProps = { 5 | class?: string; 6 | children: JSX.Element; 7 | }; 8 | 9 | /** 10 | * Button group displays multiple related actions side-by-side and helps with 11 | * arrangement and spacing. 12 | */ 13 | export function ButtonGroup(props: ButtonGroupProps) { 14 | return ( 15 |
16 | {props.children} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { For } from 'solid-js'; 2 | import { TextLink } from './TextLink'; 3 | 4 | /** 5 | * Footer with copyright notice and links to legal text. 6 | */ 7 | export function Footer() { 8 | return ( 9 |
10 |
© {new Date().getFullYear()} Fabian Hiller
11 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/components/FormFooter.tsx: -------------------------------------------------------------------------------- 1 | import { FormStore, reset } from '@modular-forms/solid'; 2 | import { ActionButton } from './ActionButton'; 3 | 4 | type FormFooterProps = { 5 | of: FormStore; 6 | }; 7 | 8 | /** 9 | * Form footer with buttons to reset and submit the form. 10 | */ 11 | export function FormFooter(props: FormFooterProps) { 12 | return ( 13 |
14 | 15 | reset(props.of)} 20 | /> 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/GitHubIconLink.tsx: -------------------------------------------------------------------------------- 1 | import { GitHubIcon } from '~/icons'; 2 | import { SystemIcon } from './SystemIcon'; 3 | 4 | /** 5 | * GitHub icon pointing to our repository. 6 | */ 7 | export function GitHubIconLink() { 8 | return ( 9 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /website/src/components/InputError.tsx: -------------------------------------------------------------------------------- 1 | import { Expandable } from './Expandable'; 2 | 3 | type InputErrorProps = { 4 | name: string; 5 | error?: string; 6 | }; 7 | 8 | /** 9 | * Input error that tells the user what to do to fix the problem. 10 | */ 11 | export function InputError(props: InputErrorProps) { 12 | return ( 13 | 14 |
18 | {props.error} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /website/src/components/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Show } from 'solid-js'; 3 | 4 | type InputLabelProps = { 5 | name: string; 6 | label?: string; 7 | required?: boolean; 8 | margin?: 'none'; 9 | }; 10 | 11 | /** 12 | * Input label for a form field. 13 | */ 14 | export function InputLabel(props: InputLabelProps) { 15 | return ( 16 | 17 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /website/src/components/SearchToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useSearch } from '~/contexts'; 2 | import { SearchIcon } from '~/icons'; 3 | import { SystemIcon } from './SystemIcon'; 4 | 5 | /** 6 | * Icon button to open the search interface of the website. 7 | */ 8 | export function SearchToggle() { 9 | const [, setOpen] = useSearch(); 10 | return ( 11 | setOpen(true)} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /website/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | type SpinnerProps = { 2 | label?: string; 3 | }; 4 | 5 | /** 6 | * Spinner provide a visual cue that an action is being processed. 7 | */ 8 | export function Spinner(props: SpinnerProps) { 9 | return ( 10 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /website/src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '~/contexts'; 2 | import { NightIcon, SunIcon } from '~/icons'; 3 | import { SystemIcon } from './SystemIcon'; 4 | 5 | /** 6 | * Button for switching the color theme. Depending on the status, a sun or 7 | * night icon is displayed. 8 | */ 9 | export function ThemeToggle() { 10 | const [getTheme, setTheme] = useTheme(); 11 | return ( 12 | setTheme((theme) => (theme === 'dark' ? 'light' : 'dark'))} 16 | > 17 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /website/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './form'; 2 | export * from './framework'; 3 | export * from './search'; 4 | export * from './theme'; 5 | -------------------------------------------------------------------------------- /website/src/cookies.ts: -------------------------------------------------------------------------------- 1 | import { createCookie } from 'solid-start'; 2 | 3 | export const frameworkCookie = createCookie('framework', { 4 | secure: import.meta.env.PROD, 5 | sameSite: 'strict', 6 | maxAge: 2592e3, // 30 days 7 | httpOnly: false, 8 | path: '/', 9 | }); 10 | -------------------------------------------------------------------------------- /website/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import { mount, StartClient } from 'solid-start/entry-client'; 2 | 3 | mount(() => , document); 4 | -------------------------------------------------------------------------------- /website/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StartServer, 3 | createHandler, 4 | renderAsync, 5 | } from 'solid-start/entry-server'; 6 | 7 | export default createHandler( 8 | renderAsync((event) => ) 9 | ); 10 | -------------------------------------------------------------------------------- /website/src/fonts/lexend-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/src/fonts/lexend-medium.woff2 -------------------------------------------------------------------------------- /website/src/fonts/lexend-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/src/fonts/lexend-regular.woff2 -------------------------------------------------------------------------------- /website/src/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function AngleDownIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/icons/AngleRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function AngleRightIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/icons/AngleUpIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function AngleUpIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function ArrowLeftIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/icons/ArrowRrightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function ArrowRightIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function CloseIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /website/src/icons/HashtagIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function HashtagIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /website/src/icons/NightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function NightIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/icons/PageIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function PageIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/icons/PenIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function PenIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function PlusIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /website/src/icons/ReactIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function ReactIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /website/src/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function SearchIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /website/src/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from 'solid-js'; 2 | 3 | export function SunIcon(props: JSX.SvgSVGAttributes) { 4 | return ( 5 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /website/src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AngleDownIcon'; 2 | export * from './AngleRightIcon'; 3 | export * from './AngleUpIcon'; 4 | export * from './ArrowLeftIcon'; 5 | export * from './ArrowRrightIcon'; 6 | export * from './CloseIcon'; 7 | export * from './GitHubIcon'; 8 | export * from './HashtagIcon'; 9 | export * from './LogoIcon'; 10 | export * from './NightIcon'; 11 | export * from './PageIcon'; 12 | export * from './PenIcon'; 13 | export * from './PlusIcon'; 14 | export * from './PreactIcon'; 15 | export * from './QwikIcon'; 16 | export * from './ReactIcon'; 17 | export * from './SearchIcon'; 18 | export * from './SolidIcon'; 19 | export * from './SunIcon'; 20 | -------------------------------------------------------------------------------- /website/src/images/blurred-code-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/src/images/blurred-code-dark.jpg -------------------------------------------------------------------------------- /website/src/images/blurred-code-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabian-hiller/modular-forms/9bdade59fbf584ada0289a8b3734c5117ab7f0a2/website/src/images/blurred-code-light.jpg -------------------------------------------------------------------------------- /website/src/images/index.ts: -------------------------------------------------------------------------------- 1 | export { default as blurredCodeDarkUrl } from './blurred-code-dark.jpg'; 2 | export { default as blurredCodeLightUrl } from './blurred-code-light.jpg'; 3 | -------------------------------------------------------------------------------- /website/src/logos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PreactLogo'; 2 | export * from './QwikLogo'; 3 | export * from './ReactLogo'; 4 | export * from './SolidLogo'; 5 | -------------------------------------------------------------------------------- /website/src/primitives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createFocusTrap'; 2 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework].tsx: -------------------------------------------------------------------------------- 1 | import { Show } from 'solid-js'; 2 | import { Meta, Outlet, useParams } from 'solid-start'; 3 | import { Framework, frameworks } from '~/contexts'; 4 | import NotFoundPage from './[...404]'; 5 | 6 | export default function FrameworkLayout() { 7 | // Use params 8 | const params = useParams(); 9 | 10 | return ( 11 | } 14 | > 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/FieldElement.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | FieldElement 4 | Type that defines the HTML element of a field. 5 | 6 | # FieldElement 7 | 8 | Type that defines the HTML element of a field. 9 | 10 | ## Definition 11 | 12 | - `FieldElement` 13 | 14 | export const properties = { 15 | FieldElement: { 16 | type: [ 17 | { 18 | type: 'custom', 19 | name: 'HTMLInputElement', 20 | }, 21 | { 22 | type: 'custom', 23 | name: 'HTMLSelectElement', 24 | }, 25 | { 26 | type: 'custom', 27 | name: 'HTMLTextAreaElement', 28 | }, 29 | ], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/FormErrors.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | FormErrors 4 | Type that defines the errors of a form. 5 | 6 | # FormErrors 7 | 8 | Type that defines the errors of a form. 9 | 10 | ## Definition 11 | 12 | - `FormErrors` 13 | 14 | ### Explanation 15 | 16 | `FormErrors` is an object that contains an entry for each field or field array with an error. The key of an entry is the name of a field or a field array and the value is the error message. 17 | 18 | export const properties = { 19 | FormErrors: { 20 | type: { 21 | type: 'object', 22 | entries: [{ key: { name: 'name', type: 'string' }, value: 'string' }], 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/Maybe.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | Maybe 4 | 5 | Utility type that transforms a type into a maybe undefined type. 6 | 7 | 8 | # Maybe 9 | 10 | Utility type that transforms a type into a maybe `undefined` type. 11 | 12 | ## Generic 13 | 14 | - `Value` 15 | 16 | ## Return 17 | 18 | - `Type` 19 | 20 | export const properties = { 21 | Value: { 22 | type: 'any', 23 | }, 24 | Type: { 25 | type: [{ type: 'custom', name: 'Value' }, 'undefined'], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/MaybeArray.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | MaybeArray 4 | 5 | Utility type that transforms a type into a maybe Array type. 6 | 7 | 8 | # MaybeArray 9 | 10 | Utility type that transforms a type into a maybe `Array` type. 11 | 12 | ## Generic 13 | 14 | - `Value` 15 | 16 | ## Return 17 | 18 | - `Type` 19 | 20 | export const properties = { 21 | Value: { 22 | type: 'any', 23 | }, 24 | Type: { 25 | type: [ 26 | { type: 'custom', name: 'Value' }, 27 | { type: 'array', item: { type: 'custom', name: 'Value' } }, 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/MaybePromise.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | MaybePromise 4 | 5 | Utility type that transforms a type into a maybe Promise type. 6 | 7 | 8 | # MaybePromise 9 | 10 | Utility type that transforms a type into a maybe `Promise` type. 11 | 12 | ## Generic 13 | 14 | - `Value` 15 | 16 | ## Return 17 | 18 | - `Type` 19 | 20 | export const properties = { 21 | Value: { 22 | type: 'any', 23 | }, 24 | Type: { 25 | type: [ 26 | { type: 'custom', name: 'Value' }, 27 | { 28 | type: 'custom', 29 | name: 'Promise', 30 | generics: [{ type: 'custom', name: 'Value' }], 31 | }, 32 | ], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/MaybeValue.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | MaybeValue 4 | 5 | Utility type that transforms a type into a maybe null or undefined type. 6 | 7 | 8 | # MaybeValue 9 | 10 | Utility type that transforms a type into a maybe `null` or `undefined` type. 11 | 12 | ## Generic 13 | 14 | - `Value` 15 | 16 | ## Return 17 | 18 | - `Type` 19 | 20 | export const properties = { 21 | Value: { 22 | type: 'any', 23 | }, 24 | Type: { 25 | type: [{ type: 'custom', name: 'Value' }, 'null', 'undefined'], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/PartialValues.mdx: -------------------------------------------------------------------------------- 1 | import { A } from 'solid-start'; 2 | import { Description, Property, Title } from '~/components'; 3 | 4 | PartialValues 5 | 6 | Utility type that transforms `FieldValues` to deep partial field values. 7 | 8 | 9 | # PartialValues 10 | 11 | Utility type that transforms `FieldValues` to deep partial field values. 12 | 13 | ## Generic 14 | 15 | - `Values` 16 | 17 | export const properties = { 18 | Values: { 19 | type: { type: 'custom', name: 'FieldValues', href: '../FieldValues' }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/ResponseData.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | ResponseData 4 | Type that defines the response data the form. 5 | 6 | # ResponseData 7 | 8 | Type that defines the response data the form. 9 | 10 | ## Definition 11 | 12 | - `ResponseData` 13 | 14 | export const properties = { 15 | ResponseData: { 16 | type: [ 17 | { 18 | type: 'custom', 19 | name: 'Record', 20 | generics: ['string', 'any'], 21 | }, 22 | { 23 | type: 'custom', 24 | name: 'Array', 25 | generics: ['any'], 26 | }, 27 | ], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/clearError.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | clearError 4 | 5 | Clears the error of the specified field or field array. 6 | 7 | 8 | # clearError 9 | 10 | Clears the error of the specified field or field array. 11 | 12 | ```ts 13 | clearError(form, name); 14 | ``` 15 | 16 | ## Parameters 17 | 18 | - `form` 19 | - `name` 20 | 21 | export const properties = { 22 | form: { 23 | type: { 24 | type: 'custom', 25 | name: 'FormStore', 26 | href: '../FormStore', 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/clearResponse.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | clearResponse 4 | Clears the response of the form. 5 | 6 | # clearResponse 7 | 8 | Clears the response of the form. 9 | 10 | ```ts 11 | clearResponse(form); 12 | ``` 13 | 14 | ## Parameters 15 | 16 | - `form` 17 | 18 | export const properties = { 19 | form: { 20 | type: { 21 | type: 'custom', 22 | name: 'FormStore', 23 | href: '../FormStore', 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/focus.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | focus 4 | Focuses the specified field of the form. 5 | 6 | # focus 7 | 8 | Focuses the specified field of the form. 9 | 10 | ```ts 11 | focus(form, name); 12 | ``` 13 | 14 | ## Parameters 15 | 16 | - `form` 17 | - `name` 18 | 19 | export const properties = { 20 | form: { 21 | type: { 22 | type: 'custom', 23 | name: 'FormStore', 24 | href: '../FormStore', 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/index.tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | 4 | export async function GET({ params }: APIEvent) { 5 | return redirect( 6 | params.framework === 'solid' 7 | ? 'createForm' 8 | : params.framework === 'qwik' 9 | ? 'formAction$' 10 | : 'useForm' 11 | ); 12 | } 13 | 14 | export default function GuidesPage() { 15 | return ( 16 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/remove.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | remove 4 | Removes a item of the field array. 5 | 6 | # remove 7 | 8 | Removes a item of the field array. 9 | 10 | ```ts 11 | remove(form, name, options); 12 | ``` 13 | 14 | ## Parameters 15 | 16 | - `form` 17 | - `name` 18 | - `options` 19 | - `at` 20 | 21 | ### Explanation 22 | 23 | With `at` you define the index of the item to be removed. The action is executed only if you specify a valid index. 24 | 25 | export const properties = { 26 | form: { 27 | type: { 28 | type: 'custom', 29 | name: 'FormStore', 30 | href: '../FormStore', 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/api/submit.mdx: -------------------------------------------------------------------------------- 1 | import { Description, Property, Title } from '~/components'; 2 | 3 | submit 4 | Validates and submits the specified form. 5 | 6 | # submit 7 | 8 | Validates and submits the specified form. 9 | 10 | ```ts 11 | submit(form); 12 | ``` 13 | 14 | ## Parameters 15 | 16 | - `form` 17 | 18 | export const properties = { 19 | form: { 20 | type: { 21 | type: 'custom', 22 | name: 'FormStore', 23 | href: '../FormStore', 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/guides/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, redirect } from 'solid-start'; 2 | 3 | export function GET() { 4 | return redirect('introduction'); 5 | } 6 | 7 | export default function GuidesPage() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/playground.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'solid-start'; 2 | import { SideBar, Debugger, Tabs } from '~/components'; 3 | import { FormProvider, useForm } from '~/contexts'; 4 | 5 | export default function PlaygroundLayout() { 6 | return ( 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/[framework]/playground/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, redirect } from 'solid-start'; 2 | 3 | export function GET() { 4 | return redirect('login'); 5 | } 6 | 7 | export default function PlaygroundPage() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/api/[...all].tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect, useParams } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request, params }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/api/${params.all}`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/api/index.tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/api`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/guides/[...all].tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect, useParams } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request, params }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/guides/${params.all}`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/guides/index.tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/guides`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/legal.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'solid-start'; 2 | 3 | export default function LegalLayout() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/playground/[...all].tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect, useParams } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request, params }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/playground/${params.all}`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/routes/(layout)/playground/index.tsx: -------------------------------------------------------------------------------- 1 | import { APIEvent, Navigate, redirect } from 'solid-start'; 2 | import { getFramework } from '~/contexts'; 3 | import { frameworkCookie } from '~/cookies'; 4 | 5 | export async function GET({ request }: APIEvent) { 6 | const framework = await frameworkCookie.parse(request.headers.get('Cookie')); 7 | return redirect(`/${framework || 'solid'}/playground`); 8 | } 9 | 10 | export default function GuidesPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /website/src/styles/components.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | -------------------------------------------------------------------------------- /website/src/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lexend'; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url(../fonts/lexend-regular.woff2) format('woff2'); 7 | } 8 | @font-face { 9 | font-family: 'Lexend'; 10 | font-style: normal; 11 | font-weight: 500; 12 | font-display: swap; 13 | src: url(../fonts/lexend-medium.woff2) format('woff2'); 14 | } 15 | -------------------------------------------------------------------------------- /website/src/utils/disableTransitions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Disables CSS transitions for a short moment. 3 | */ 4 | export function disableTransitions() { 5 | const { classList } = document.documentElement; 6 | classList.add('disable-transitions'); 7 | setTimeout(() => classList.remove('disable-transitions'), 100); 8 | } 9 | -------------------------------------------------------------------------------- /website/src/utils/getFrameworkName.ts: -------------------------------------------------------------------------------- 1 | import { Framework } from '~/contexts'; 2 | 3 | /** 4 | * Returns the name of the framework by its identifier. 5 | * 6 | * @param framework The framework identifier. 7 | * 8 | * @returns The name of the Framework 9 | */ 10 | export function getFrameworkName(framework: Framework) { 11 | return { solid: 'SolidJS', qwik: 'Qwik', preact: 'Preact', react: 'React' }[ 12 | framework 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /website/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disableTransitions'; 2 | export * from './getFrameworkName'; 3 | export * from './trackEvent'; 4 | -------------------------------------------------------------------------------- /website/src/utils/trackEvent.ts: -------------------------------------------------------------------------------- 1 | type EventName = 'switch_framework' | 'open_search' | 'select_search_item'; 2 | type EventData = { [key: string]: string | number | boolean | undefined }; 3 | 4 | declare global { 5 | interface Window { 6 | umami?: { track: (name: EventName, data?: EventData) => void }; 7 | } 8 | } 9 | 10 | /** 11 | * Tracks custom Umami events. 12 | * 13 | * @param name The event name. 14 | * @param data The event data. 15 | */ 16 | export function trackEvent(name: EventName, data?: EventData) { 17 | window.umami?.track(name, data); 18 | } 19 | -------------------------------------------------------------------------------- /website/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: ['./src/**/*.{ts,tsx,mdx}'], 5 | darkMode: 'class', 6 | future: { 7 | hoverOnlyWhenSupported: true, 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | lexend: ['"Lexend"', ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "jsxImportSource": "solid-js", 10 | "jsx": "preserve", 11 | "types": ["vite/client"], 12 | "baseUrl": "./", 13 | "paths": { 14 | "~/*": ["./src/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /website/vite.config.ts: -------------------------------------------------------------------------------- 1 | import rehypePrism from '@mapbox/rehype-prism'; 2 | import mdx from '@mdx-js/rollup'; 3 | import rehypeSlug from 'rehype-slug'; 4 | import solid from 'solid-start/vite'; 5 | // @ts-ignore 6 | import netlify from 'solid-start-netlify'; 7 | import { defineConfig } from 'vite'; 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | // @ts-ignore 12 | { 13 | ...mdx({ 14 | jsx: true, 15 | jsxImportSource: 'solid-js', 16 | providerImportSource: 'solid-mdx', 17 | rehypePlugins: [rehypeSlug, rehypePrism], 18 | }), 19 | enforce: 'pre', 20 | }, 21 | solid({ 22 | adapter: netlify({ edge: false }), 23 | extensions: ['.tsx', '.mdx'], 24 | ssr: true, 25 | }), 26 | ], 27 | }); 28 | --------------------------------------------------------------------------------