diff --git a/commitlint.config.js b/commitlint.config.js
index 4698a12..e40d319 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -1,6 +1,6 @@
export default {
extends: ['@commitlint/config-conventional'],
rules: {
- 'footer-max-length': [2, 'never']
+ 'footer-max-length': [0]
}
};
diff --git a/src/components/Form/PdapForm.vue b/src/components/Form/PdapForm.vue
index cf3c4bf..bd16378 100644
--- a/src/components/Form/PdapForm.vue
+++ b/src/components/Form/PdapForm.vue
@@ -163,6 +163,9 @@ async function submit(e: Event) {
* The `Form` component is powerful. All you need to do is pass a few props, and the component will generate inputs and render them in the UI, complete with customizable form validation and both form-level and input-level error states.
*
*
+ * @deprecated use FormV2 with the PdapInputCheckbox, ...Text, and ...Password components instead
+ *
+ *
* ## Props
* @prop {string | undefined | null} error Error state. Only a non-falsy string results in a form-level error being displayed
* @prop {string} id Passed through to the `form` element as its `id`
diff --git a/src/components/FormV2/PdapFormV2.vue b/src/components/FormV2/PdapFormV2.vue
new file mode 100644
index 0000000..5eb43b3
--- /dev/null
+++ b/src/components/FormV2/PdapFormV2.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
diff --git a/src/components/FormV2/__snapshots__/formv2.spec.ts.snap b/src/components/FormV2/__snapshots__/formv2.spec.ts.snap
new file mode 100644
index 0000000..b04c310
--- /dev/null
+++ b/src/components/FormV2/__snapshots__/formv2.spec.ts.snap
@@ -0,0 +1,139 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`PdapFormV2 > calls submit event with form values on valid submission 1`] = `
+
+`;
+
+exports[`PdapFormV2 > renders default error message when form has errors 1`] = `
+
+`;
+
+exports[`PdapFormV2 > renders error message slot when provided 1`] = `
+
+`;
+
+exports[`PdapFormV2 > renders error message when errorMessage prop is provided 1`] = `
+
+`;
+
+exports[`PdapFormV2 > renders the form element 1`] = `
+
+`;
diff --git a/src/components/FormV2/formv2.spec.ts b/src/components/FormV2/formv2.spec.ts
new file mode 100644
index 0000000..bd46453
--- /dev/null
+++ b/src/components/FormV2/formv2.spec.ts
@@ -0,0 +1,183 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import PdapFormV2 from './PdapFormV2.vue';
+import InputCheckbox from '../InputCheckbox/PdapInputCheckbox.vue';
+import InputText from '../InputText/PdapInputText.vue';
+import InputPassword from '../InputPassword/PdapInputPassword.vue';
+
+vi.mock('vue-router');
+vi.mock('vue', async () => {
+ const actual: Record = await vi.importActual('vue');
+ return {
+ ...actual,
+ /** Shim for `exportHelper` function which throws errors on Input because actual
+ * Vue impl does not have optional chaining on `sfc.__vccOpts` check */
+ exportHelper: function (
+ sfc: Record,
+ props: [string, string][]
+ ) {
+ const newObject = sfc?.vccOpts ?? sfc;
+ for (const [key, val] of props) {
+ newObject[key] = val;
+ }
+ return newObject;
+ },
+ };
+});
+vi.mock('@vuelidate/core', async () => {
+ const actual: Record =
+ await vi.importActual('@vuelidate/core');
+
+ return {
+ ...actual,
+ };
+});
+
+const submit = vi.fn((values: Record, e: Event) => ({
+ values,
+ e,
+}));
+
+const BASE_CONFIG = {
+ props: {
+ defaultValues: {
+ name: '',
+ email: '',
+ password: '',
+ 'ice-cream': false,
+ },
+ schema: [
+ {
+ name: 'name',
+ validators: {
+ required: {
+ value: true,
+ },
+ },
+ },
+ {
+ name: 'email',
+ validators: {
+ required: { value: true },
+ email: { value: true },
+ },
+ },
+ {
+ name: 'password',
+ validators: {
+ password: { value: true, message: 'Password is too weak' },
+ },
+ },
+ ],
+ id: 'test',
+ name: 'test',
+ },
+ attrs: {
+ onSubmit: submit,
+ },
+ slots: {
+ default: `
+
+
+
+
+ `,
+ },
+ global: {
+ stubs: {
+ InputCheckbox,
+ InputText,
+ InputPassword,
+ },
+ },
+};
+
+describe('PdapFormV2', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(PdapFormV2, BASE_CONFIG);
+ });
+
+ it('renders the form element', () => {
+ expect(wrapper.find('form').exists()).toBe(true);
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders error message slot when provided', () => {
+ wrapper = mount(PdapFormV2, {
+ ...BASE_CONFIG,
+ slots: {
+ error: 'Custom Error Message
',
+ },
+ });
+ expect(wrapper.find('.pdap-form-error-message').exists()).toBe(false);
+ expect(wrapper.text()).toContain('Custom Error Message');
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders error message when errorMessage prop is provided', () => {
+ wrapper = mount(PdapFormV2, {
+ ...BASE_CONFIG,
+ props: {
+ ...BASE_CONFIG.props,
+ error: 'Form Error',
+ },
+ });
+ expect(wrapper.find('.pdap-form-error-message').text()).toBe('Form Error');
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders default error message when form has errors', async () => {
+ wrapper.find('form').trigger('submit');
+ await nextTick();
+ expect(wrapper.find('.pdap-form-error-message').text()).toBe(
+ 'Please update this form to correct the errors'
+ );
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('calls submit event with form values on valid submission', async () => {
+ const form = await wrapper.find('form');
+ await form.find('input[name="name"]').setValue('John Doe');
+ await form.find('input[name="email"]').setValue('john@example.com');
+ await form.find('input[name="password"]').setValue('Password123!');
+ await form.find('input[name="ice-cream"]').setChecked();
+
+ await form.trigger('submit');
+ await wrapper.vm.$forceUpdate();
+ await wrapper.vm.$nextTick();
+
+ expect(submit).toHaveBeenCalledWith(
+ {
+ name: 'John Doe',
+ email: 'john@example.com',
+ password: 'Password123!',
+ 'ice-cream': true,
+ },
+ expect.any(Event)
+ );
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/FormV2/index.ts b/src/components/FormV2/index.ts
new file mode 100644
index 0000000..d3f6fd0
--- /dev/null
+++ b/src/components/FormV2/index.ts
@@ -0,0 +1 @@
+export { default as FormV2 } from './PdapFormV2.vue';
diff --git a/src/components/FormV2/types.ts b/src/components/FormV2/types.ts
new file mode 100644
index 0000000..6d1ae4e
--- /dev/null
+++ b/src/components/FormV2/types.ts
@@ -0,0 +1,47 @@
+// TODO: remove the V2 from all of these types when Form is removed and FormV2 -> Form
+
+import useVuelidate from '@vuelidate/core';
+import { makeRules } from './util';
+import { Ref } from 'vue';
+
+export interface PdapFormValidatorV2 {
+ message?: string;
+ value: T;
+}
+
+/**
+ * Keyed by currently used validators.
+ * Add any Vuelidate validators or custom ones here as we need them.
+ * See https://vuelidate-next.netlify.app/validators.html#using-builtin-validators for more.
+ *
+ */
+export interface PdapFormValidatorsV2 {
+ maxLength: PdapFormValidatorV2;
+ minLength: PdapFormValidatorV2;
+ required: PdapFormValidatorV2;
+ email: PdapFormValidatorV2;
+ password: PdapFormValidatorV2;
+}
+
+export type ValidationSchemaV2 = {
+ name: string;
+ validators: Partial;
+}[];
+/**
+ * PDAP Form props interface.
+ */
+export interface PdapFormPropsV2 {
+ defaultValues?: Record;
+ error?: string | undefined | null;
+ // Adds id and name in order to make required
+ id: string;
+ name: string;
+ schema: ValidationSchemaV2;
+}
+
+export interface PdapFormProvideV2 {
+ values: Ref>;
+ setValues: (values: Record) => void;
+ rules: ReturnType;
+ v$: ReturnType;
+}
diff --git a/src/components/FormV2/util.ts b/src/components/FormV2/util.ts
new file mode 100644
index 0000000..ca682fb
--- /dev/null
+++ b/src/components/FormV2/util.ts
@@ -0,0 +1,25 @@
+import { createRule } from '../../utils/vuelidate';
+import { PdapFormProvideV2, ValidationSchemaV2 } from './types';
+import { InjectionKey } from 'vue';
+
+export function makeRules(
+ schema: ValidationSchemaV2
+): Record> {
+ return schema.reduce((acc, { name, validators }) => {
+ const toAdd = Object.entries(validators ?? {}).reduce((acc, [key, val]) => {
+ return {
+ ...acc,
+ ...createRule(key, val),
+ };
+ }, {});
+
+ return {
+ ...acc,
+ [name]: {
+ ...toAdd,
+ },
+ };
+ }, {});
+}
+
+export const provideKey = Symbol() as InjectionKey;
diff --git a/src/components/Input/PdapInput.vue b/src/components/Input/PdapInput.vue
index fdcf99e..f4ff281 100644
--- a/src/components/Input/PdapInput.vue
+++ b/src/components/Input/PdapInput.vue
@@ -78,9 +78,7 @@ const errorMessageId = computed(() => `pdap-${props.name}-input-error`);
/* Error state */
.pdap-input-error {
- @apply flex-wrap;
-
- row-gap: 0;
+ @apply flex-wrap gap-x-0;
}
.pdap-input-error label {
diff --git a/src/components/InputCheckbox/PdapInputCheckbox.vue b/src/components/InputCheckbox/PdapInputCheckbox.vue
new file mode 100644
index 0000000..6387859
--- /dev/null
+++ b/src/components/InputCheckbox/PdapInputCheckbox.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
diff --git a/src/components/InputCheckbox/index.ts b/src/components/InputCheckbox/index.ts
new file mode 100644
index 0000000..ebdb38b
--- /dev/null
+++ b/src/components/InputCheckbox/index.ts
@@ -0,0 +1 @@
+export { default as InputCheckbox } from './PdapInputCheckbox.vue';
diff --git a/src/components/InputCheckbox/types.ts b/src/components/InputCheckbox/types.ts
new file mode 100644
index 0000000..691e4ab
--- /dev/null
+++ b/src/components/InputCheckbox/types.ts
@@ -0,0 +1,6 @@
+export interface PdapInputCheckboxProps {
+ id: string;
+ label?: string;
+ name: string;
+ defaultChecked?: boolean;
+}
diff --git a/src/components/InputPassword/PdapInputPassword.vue b/src/components/InputPassword/PdapInputPassword.vue
new file mode 100644
index 0000000..9e22d29
--- /dev/null
+++ b/src/components/InputPassword/PdapInputPassword.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/src/components/InputPassword/index.ts b/src/components/InputPassword/index.ts
new file mode 100644
index 0000000..0e15271
--- /dev/null
+++ b/src/components/InputPassword/index.ts
@@ -0,0 +1 @@
+export { default as InputPassword } from './PdapInputPassword.vue';
diff --git a/src/components/InputText/PdapInputText.vue b/src/components/InputText/PdapInputText.vue
new file mode 100644
index 0000000..0d0c05d
--- /dev/null
+++ b/src/components/InputText/PdapInputText.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/src/components/InputText/index.ts b/src/components/InputText/index.ts
new file mode 100644
index 0000000..9cfa813
--- /dev/null
+++ b/src/components/InputText/index.ts
@@ -0,0 +1 @@
+export { default as InputText } from './PdapInputText.vue';
diff --git a/src/components/InputText/types.ts b/src/components/InputText/types.ts
new file mode 100644
index 0000000..99ba44b
--- /dev/null
+++ b/src/components/InputText/types.ts
@@ -0,0 +1,6 @@
+export interface PdapInputTextProps {
+ id: string;
+ label?: string;
+ name: string;
+ placeholder?: string;
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index d54b1d3..45b1027 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -2,7 +2,11 @@ export { Button } from './Button';
export { ErrorBoundary } from './ErrorBoundary';
export { Footer } from './Footer';
export { Form } from './Form';
+export { FormV2 } from './FormV2';
export { Input } from './Input';
+export { InputCheckbox } from './InputCheckbox';
+export { InputPassword } from './InputPassword';
+export { InputText } from './InputText';
export { Header } from './Header';
export { Nav } from './Nav';
export { QuickSearchForm } from './QuickSearchForm';
diff --git a/src/demo/pages/FormV2Demo.vue b/src/demo/pages/FormV2Demo.vue
new file mode 100644
index 0000000..5f9a1f1
--- /dev/null
+++ b/src/demo/pages/FormV2Demo.vue
@@ -0,0 +1,80 @@
+
+
+ console.log({ values })"
+ >
+
+
+ Your name
+
+
+
+
+
+ Foo bar baz, extra content here
+
+
+
+ Submit
+
+
+
+
+
+
+
diff --git a/src/demo/router.js b/src/demo/router.js
index 1dd200d..89fd514 100644
--- a/src/demo/router.js
+++ b/src/demo/router.js
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router';
import ComponentDemo from './pages/ComponentDemo.vue';
import SignupFormDemo from './pages/SignupFormDemo.vue';
+import FormV2Demo from './pages/FormV2Demo.vue';
const routes = [
{
@@ -46,6 +47,11 @@ const routes = [
component: SignupFormDemo,
name: 'Login Demo',
},
+ {
+ path: '/form-v2-demo',
+ component: FormV2Demo,
+ name: 'FormV2 Demo',
+ },
];
const router = createRouter({
diff --git a/src/index.ts b/src/index.ts
index cf2bcaf..0be052d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,7 @@ export * from './components/Dropdown/types';
export * from './components/ErrorBoundary/types';
export * from './components/Footer/types';
export * from './components/Form/types';
+export * from './components/FormV2/types';
export * from './components/Header/types';
export * from './components/Input/types';
export * from './components/Nav/types';
diff --git a/src/styles/components.css b/src/styles/components.css
index 79fb44d..3f9946e 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -35,4 +35,74 @@
.pdap-flex-container-center {
@apply pdap-flex-container items-center justify-center;
}
+
+ /* Input styles */
+ .pdap-input {
+ @apply h-[max-content] gap-1 leading-normal mb-3 w-full flex flex-col;
+ }
+
+ .pdap-input input {
+ @apply dark:bg-neutral-950 border border-neutral-500 border-solid px-3 py-2 text-[rgba(0,0,0)];
+ }
+
+ .pdap-input input::placeholder {
+ @apply text-neutral-600 text-lg;
+ }
+
+ .pdap-input input:focus,
+ .pdap-input input:focus-within,
+ .pdap-input input:focus-visible {
+ @apply border-2 border-blue-light border-solid outline-none;
+ }
+
+ .pdap-input label {
+ @apply max-w-[max-content] text-lg py-1 font-medium;
+ }
+
+ /* Error state */
+ .pdap-input-error {
+ @apply flex-wrap gap-x-0;
+ }
+
+ .pdap-input-error label {
+ @apply justify-start;
+ }
+
+ .pdap-input-error input {
+ @apply border-red-800 dark:border-red-300;
+ }
+
+ .pdap-input-error-message {
+ @apply items-center justify-start flex bg-red-300 text-red-800 p-1 text-xs;
+ }
+
+ /* Specific inputs */
+ /* Input - text */
+ .pdap-input input[type='text'],
+ .pdap-input input[type='password'] {
+ @apply h-12 text-lg;
+ }
+
+ /* Input - checkbox */
+ .pdap-input-checkbox {
+ @apply border-2 border-transparent items-center gap-4 flex-row py-1 px-2 w-auto;
+ }
+
+ .pdap-input-checkbox:has(input:checked) {
+ @apply border-2 border-brand-gold border-solid rounded-md;
+ }
+
+ .pdap-input input[type='checkbox'] {
+ @apply h-6 w-6 accent-brand-gold;
+ }
+
+ .pdap-input input[type='checkbox'] ~ label {
+ @apply pl-0 w-full max-w-full;
+ }
+
+ .pdap-input input[type='checkbox'],
+ .pdap-input input[type='checkbox'] ~ label {
+ @apply cursor-pointer;
+ }
+
}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 9bcaa38..270fba7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,10 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
- "ES2020",
+ "ES2022",
"DOM",
"DOM.Iterable"
],