Add `forms` demo page

pull/6632/head
Mert Sincan 2024-10-23 05:05:27 +01:00
parent 9f8003ad9d
commit 4929f978f2
24 changed files with 3002 additions and 22 deletions

View File

@ -497,6 +497,12 @@
} }
] ]
}, },
{
"name": "Forms",
"icon": "pi pi-check-circle",
"to": "/forms",
"badge": "NEW"
},
{ {
"name": "Pass Through", "name": "Pass Through",
"icon": "pi pi-directions", "icon": "pi pi-directions",

View File

@ -69,7 +69,7 @@ export default {
interfaces = this.findOtherInterfaces(values, docName); interfaces = this.findOtherInterfaces(values, docName);
} }
const types = APIDocs[moduleName]['types']; const types = APIDocs[moduleName]?.['types'];
const services = modelValues; // (TerminalService && ConfirmationService && ToastService) const services = modelValues; // (TerminalService && ConfirmationService && ToastService)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
<template>
<DocSectionText id="accessibility" label="Accessibility" v-bind="$attrs">
<h3>Screen Reader</h3>
<p>Form does not require any roles and attributes.</p>
<h3>Keyboard Support</h3>
<p>Component does not include any interactive elements.</p>
</DocSectionText>
</template>

View File

@ -0,0 +1,115 @@
<template>
<DocSectionText v-bind="$attrs">
<p>
Form is compatible with PrimeVue components, enabling smooth integration and functionality. Each component is linked to Form via a <i>name</i> property, which the form uses to create a state object for tracking values, errors and actions.
</p>
</DocSectionText>
<div class="card flex justify-center">
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</div>
<DocSectionCode :code="code" />
</template>
<script>
export default {
data() {
return {
initialValues: {
username: ''
},
code: {
basic: `
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
`,
options: `
<template>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</template>
<script>
export default {
data() {
return {
initialValues: {
username: ''
}
};
},
methods: {
resolver: ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
<\/script>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
resolver: ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
</script>

View File

@ -0,0 +1,267 @@
<template>
<DocSectionText v-bind="$attrs">
<p>This section demonstrates how to create a dynamic form using a custom Form component. It showcases an example where form fields are generated dynamically based on the provided configuration, allowing for flexible form structures.</p>
</DocSectionText>
<div class="card grid md:grid-cols-2 gap-4 w-full">
<Fieldset legend="Form 1" pt:content:class="flex justify-center">
<DynamicForm @submit="onFormSubmit('Form 1', $event)">
<DynamicFormField groupId="userId_1" name="username">
<DynamicFormLabel>Username</DynamicFormLabel>
<DynamicFormControl defaultValue="PrimeVue" fluid :schema="userNameSchema" />
<DynamicFormMessage />
</DynamicFormField>
<DynamicFormField groupId="passId_1" name="password">
<DynamicFormLabel>Password</DynamicFormLabel>
<DynamicFormControl as="Password" :feedback="false" fluid :schema="passwordSchema" />
<DynamicFormMessage errorType="minimum" />
<DynamicFormMessage errorType="maximum" />
<DynamicFormMessage errorType="uppercase" severity="warn" />
<DynamicFormMessage errorType="lowercase" severity="warn" />
<DynamicFormMessage errorType="number" severity="secondary" />
</DynamicFormField>
<DynamicFormSubmit />
</DynamicForm>
</Fieldset>
<Fieldset legend="Form 2" pt:content:class="flex justify-center">
<DynamicForm :fields @submit="onFormSubmit('Form 2', $event)" />
</Fieldset>
</div>
<DocSectionCode :code="code" />
</template>
<script>
import { markRaw } from 'vue';
import { z } from 'zod';
import DynamicForm from './dynamic/DynamicForm.vue';
import DynamicFormControl from './dynamic/DynamicFormControl.vue';
import DynamicFormField from './dynamic/DynamicFormField.vue';
import DynamicFormLabel from './dynamic/DynamicFormLabel.vue';
import DynamicFormMessage from './dynamic/DynamicFormMessage.vue';
import DynamicFormSubmit from './dynamic/DynamicFormSubmit.vue';
export default {
data() {
return {
userNameSchema: z.string().min(1, { message: 'Username is required' }),
passwordSchema: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
errorType: 'lowercase',
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
errorType: 'uppercase',
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
errorType: 'number',
message: 'Password must contain at least one number.'
}),
fields: {
username: {
groupId: 'userId_2',
label: 'Username',
defaultValue: 'PrimeVue',
fluid: true,
schema: z.string().min(1, { message: 'Username is required' })
},
password: {
groupId: 'passId_2',
label: 'Password',
as: 'Password',
feedback: false,
fluid: true,
messages: [{ errorType: 'minimum' }, { errorType: 'maximum' }, { errorType: 'uppercase', severity: 'warn' }, { errorType: 'lowercase', severity: 'warn' }, { errorType: 'number', severity: 'secondary' }],
schema: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
errorType: 'lowercase',
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
errorType: 'uppercase',
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
errorType: 'number',
message: 'Password must contain at least one number.'
})
}
},
code: {
basic: `
<Fieldset legend="Form 1" pt:content:class="flex justify-center">
<DynamicForm @submit="onFormSubmit('Form 1', $event)">
<DynamicFormField groupId="userId_1" name="username">
<DynamicFormLabel>Username</DynamicFormLabel>
<DynamicFormControl defaultValue="PrimeVue" fluid :schema="userNameSchema" />
<DynamicFormMessage />
</DynamicFormField>
<DynamicFormField groupId="passId_1" name="password">
<DynamicFormLabel>Password</DynamicFormLabel>
<DynamicFormControl as="Password" :feedback="false" fluid :schema="passwordSchema" />
<DynamicFormMessage errorType="minimum" />
<DynamicFormMessage errorType="maximum" />
<DynamicFormMessage errorType="uppercase" severity="warn" />
<DynamicFormMessage errorType="lowercase" severity="warn" />
<DynamicFormMessage errorType="number" severity="secondary" />
</DynamicFormField>
<DynamicFormSubmit />
</DynamicForm>
</Fieldset>
<Fieldset legend="Form 2" pt:content:class="flex justify-center">
<DynamicForm :fields @submit="onFormSubmit('Form 2', $event)" />
</Fieldset>
`,
options: `
<template>
<Fieldset legend="Form 1" pt:content:class="flex justify-center">
<DynamicForm @submit="onFormSubmit('Form 1', $event)">
<DynamicFormField groupId="userId_1" name="username">
<DynamicFormLabel>Username</DynamicFormLabel>
<DynamicFormControl defaultValue="PrimeVue" fluid :schema="userNameSchema" />
<DynamicFormMessage />
</DynamicFormField>
<DynamicFormField groupId="passId_1" name="password">
<DynamicFormLabel>Password</DynamicFormLabel>
<DynamicFormControl as="Password" :feedback="false" fluid :schema="passwordSchema" />
<DynamicFormMessage errorType="minimum" />
<DynamicFormMessage errorType="maximum" />
<DynamicFormMessage errorType="uppercase" severity="warn" />
<DynamicFormMessage errorType="lowercase" severity="warn" />
<DynamicFormMessage errorType="number" severity="secondary" />
</DynamicFormField>
<DynamicFormSubmit />
</DynamicForm>
</Fieldset>
<Fieldset legend="Form 2" pt:content:class="flex justify-center">
<DynamicForm :fields @submit="onFormSubmit('Form 2', $event)" />
</Fieldset>
</template>
<script>
import { markRaw } from 'vue';
import { z } from 'zod';
import DynamicForm from './dynamic/DynamicForm.vue';
import DynamicFormControl from './dynamic/DynamicFormControl.vue';
import DynamicFormField from './dynamic/DynamicFormField.vue';
import DynamicFormLabel from './dynamic/DynamicFormLabel.vue';
import DynamicFormMessage from './dynamic/DynamicFormMessage.vue';
import DynamicFormSubmit from './dynamic/DynamicFormSubmit.vue';
export default {
data() {
return {
userNameSchema: z.string().min(1, { message: 'Username is required' }),
passwordSchema: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
errorType: 'lowercase',
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
errorType: 'uppercase',
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
errorType: 'number',
message: 'Password must contain at least one number.'
}),
fields: {
username: {
groupId: 'userId_2',
label: 'Username',
defaultValue: 'PrimeVue',
fluid: true,
schema: z.string().min(1, { message: 'Username is required' })
},
password: {
groupId: 'passId_2',
label: 'Password',
as: 'Password',
feedback: false,
fluid: true,
messages: [{ errorType: 'minimum' }, { errorType: 'maximum' }, { errorType: 'uppercase', severity: 'warn' }, { errorType: 'lowercase', severity: 'warn' }, { errorType: 'number', severity: 'secondary' }],
schema: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
errorType: 'lowercase',
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
errorType: 'uppercase',
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
errorType: 'number',
message: 'Password must contain at least one number.'
})
}
}
};
},
methods: {
onFormSubmit(text, { valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: \`\${text} is submitted.\`, life: 3000 });
}
}
},
components: {
DynamicForm: markRaw(DynamicForm),
DynamicFormControl: markRaw(DynamicFormControl),
DynamicFormField: markRaw(DynamicFormField),
DynamicFormLabel: markRaw(DynamicFormLabel),
DynamicFormMessage: markRaw(DynamicFormMessage),
DynamicFormSubmit: markRaw(DynamicFormSubmit)
}
};
<\/script>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
onFormSubmit(text, { valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: `${text} is submitted.`, life: 3000 });
}
}
},
components: {
DynamicForm: markRaw(DynamicForm),
DynamicFormControl: markRaw(DynamicFormControl),
DynamicFormField: markRaw(DynamicFormField),
DynamicFormLabel: markRaw(DynamicFormLabel),
DynamicFormMessage: markRaw(DynamicFormMessage),
DynamicFormSubmit: markRaw(DynamicFormSubmit)
}
};
</script>

View File

@ -0,0 +1,18 @@
<template>
<DocSectionText v-bind="$attrs" />
<DocSectionCode :code="code" hideToggleCode importCode hideStackBlitz />
</template>
<script>
export default {
data() {
return {
code: {
basic: `
import Form from '@primevue/form';
`
}
};
}
};
</script>

View File

@ -0,0 +1,125 @@
<template>
<DocSectionText v-bind="$attrs">
<p>
The <i>register</i> callback allows non-PrimeVue components to be registered within Form component. This enables custom elements to participate in the form's validation and value tracking, ensuring they work alongside PrimeVue components.
</p>
</DocSectionText>
<div class="card flex justify-center">
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<input name="username" type="text" placeholder="Username" v-bind="$form.register('username')" />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</div>
<DocSectionCode :code="code" />
</template>
<script>
import { zodResolver } from '@primevue/form/resolvers';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: ''
},
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required' })
})
),
code: {
basic: `
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<input name="username" type="text" placeholder="Username" v-bind="$form.register('username')" />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
`,
options: `
<template>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<input name="username" type="text" placeholder="Username" v-bind="$form.register('username')" />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</template>
<script>
import { zodResolver } from '@primevue/form/resolvers';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: ''
},
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required' })
})
)
};
},
methods: {
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
<\/script>
<style scoped>
input {
width: 100%;
color: var(--p-inputtext-color);
background: var(--p-inputtext-background);
border: 1px solid var(--p-inputtext-border-color);
}
<\/style>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
</script>
<style scoped>
input {
width: 100%;
color: var(--p-inputtext-color);
background: var(--p-inputtext-background);
border: 1px solid var(--p-inputtext-border-color);
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<DocSectionText v-bind="$attrs">
<p>
It can be integrated with with schema libraries such as <a href="https://zod.dev/">Zod</a>, <a href="https://github.com/jquense/yup">Yup</a>, <a href="https://joi.dev/">Joi</a>, <a href="https://valibot.dev/">Valibot</a>,
<a href="https://docs.superstructjs.org/">Superstruct</a> or custom validation logic is possible using <i>resolver</i> property, with available resolvers from <i>@primevue/form/resolvers</i> for each schema.
</p>
</DocSectionText>
<div class="card flex flex-col items-center gap-5">
<Fieldset legend="Schema">
<RadioButtonGroup v-model="selectedSchema" name="schema" class="flex flex-wrap gap-4" @update:modelValue="changeResolver">
<div class="flex items-center">
<RadioButton inputId="zod" value="Zod" />
<label for="zod" class="ml-2">Zod</label>
</div>
<div class="flex items-center">
<RadioButton inputId="yup" value="Yup" />
<label for="yup" class="ml-2">Yup</label>
</div>
<div class="flex items-center">
<RadioButton inputId="valibot" value="Valibot" />
<label for="valibot" class="ml-2">Valibot</label>
</div>
<div class="flex items-center">
<RadioButton inputId="superStruct" value="SuperStruct" />
<label for="superStruct" class="ml-2">SuperStruct</label>
</div>
<div class="flex items-center">
<RadioButton inputId="custom" value="Custom" />
<label for="custom" class="ml-2">Custom</label>
</div>
</RadioButtonGroup>
</Fieldset>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</div>
<DocSectionCode :code="code" />
</template>
<script>
import { superStructResolver, valibotResolver, yupResolver, zodResolver } from '@primevue/form/resolvers';
import * as s from 'superstruct';
import * as v from 'valibot';
import * as yup from 'yup';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: ''
},
selectedSchema: 'Zod',
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required via Zod.' })
})
),
code: {
basic: `
<Fieldset legend="Schema">
<RadioButtonGroup v-model="selectedSchema" name="schema" class="flex flex-wrap gap-4" @update:modelValue="changeResolver">
<div class="flex items-center">
<RadioButton inputId="zod" value="Zod" />
<label for="zod" class="ml-2">Zod</label>
</div>
<div class="flex items-center">
<RadioButton inputId="yup" value="Yup" />
<label for="yup" class="ml-2">Yup</label>
</div>
<div class="flex items-center">
<RadioButton inputId="valibot" value="Valibot" />
<label for="valibot" class="ml-2">Valibot</label>
</div>
<div class="flex items-center">
<RadioButton inputId="superStruct" value="SuperStruct" />
<label for="superStruct" class="ml-2">SuperStruct</label>
</div>
<div class="flex items-center">
<RadioButton inputId="custom" value="Custom" />
<label for="custom" class="ml-2">Custom</label>
</div>
</RadioButtonGroup>
</Fieldset>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
`,
options: `
<template>
<Fieldset legend="Schema">
<RadioButtonGroup v-model="selectedSchema" name="schema" class="flex flex-wrap gap-4" @update:modelValue="changeResolver">
<div class="flex items-center">
<RadioButton inputId="zod" value="Zod" />
<label for="zod" class="ml-2">Zod</label>
</div>
<div class="flex items-center">
<RadioButton inputId="yup" value="Yup" />
<label for="yup" class="ml-2">Yup</label>
</div>
<div class="flex items-center">
<RadioButton inputId="valibot" value="Valibot" />
<label for="valibot" class="ml-2">Valibot</label>
</div>
<div class="flex items-center">
<RadioButton inputId="superStruct" value="SuperStruct" />
<label for="superStruct" class="ml-2">SuperStruct</label>
</div>
<div class="flex items-center">
<RadioButton inputId="custom" value="Custom" />
<label for="custom" class="ml-2">Custom</label>
</div>
</RadioButtonGroup>
</Fieldset>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</template>
<script>
import { superStructResolver, valibotResolver, yupResolver, zodResolver } from '@primevue/form/resolvers';
import * as s from 'superstruct';
import * as v from 'valibot';
import * as yup from 'yup';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: ''
},
selectedSchema: 'Zod',
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required via Zod.' })
})
)
};
},
methods: {
changeResolver(schema) {
if (schema === 'Zod') {
this.resolver = zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required via Zod.' })
})
);
} else if (schema === 'Yup') {
this.resolver = yupResolver(
yup.object().shape({
username: yup.string().required('Username is required via Yup.')
})
);
} else if (schema === 'Valibot') {
this.resolver = valibotResolver(
v.object({
username: v.pipe(v.string(), v.minLength(1, 'Username is required via Valibot.'))
})
);
} else if (schema === 'SuperStruct') {
this.resolver = superStructResolver(
s.object({
username: s.nonempty(s.string())
})
);
} else if (schema === 'Custom') {
this.resolver = ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
};
}
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
<\/script>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
changeResolver(schema) {
if (schema === 'Zod') {
this.resolver = zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required via Zod.' })
})
);
} else if (schema === 'Yup') {
this.resolver = yupResolver(
yup.object().shape({
username: yup.string().required('Username is required via Yup.')
})
);
} else if (schema === 'Valibot') {
this.resolver = valibotResolver(
v.object({
username: v.pipe(v.string(), v.minLength(1, 'Username is required via Valibot.'))
})
);
} else if (schema === 'SuperStruct') {
this.resolver = superStructResolver(
s.object({
username: s.nonempty(s.string())
})
);
} else if (schema === 'Custom') {
this.resolver = ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
};
}
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
</script>

View File

@ -0,0 +1,119 @@
<template>
<DocSectionText v-bind="$attrs">
<p>Form uses the <i>name</i> property to create a state object for tracking values, errors, and actions.</p>
</DocSectionText>
<div class="card flex justify-center">
<Form v-slot="$form" :initialValues="initialValues" :resolver="resolver" @submit="onFormSubmit" class="grid md:grid-cols-2 gap-4 w-full">
<div class="flex flex-col justify-center items-center gap-4">
<InputText name="username" type="text" placeholder="Username" />
<Button type="submit" severity="secondary" label="Submit" />
</div>
<Fieldset legend="Form States">
<pre>{{ $form }}</pre>
</Fieldset>
</Form>
</div>
<DocSectionCode :code="code" />
</template>
<script>
export default {
data() {
return {
initialValues: {
username: ''
},
code: {
basic: `
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="grid md:grid-cols-2 gap-4 w-full">
<div class="flex flex-col justify-center items-center gap-4">
<InputText name="username" type="text" placeholder="Username" />
<Button type="submit" severity="secondary" label="Submit" />
</div>
<Fieldset legend="Form States">
<pre>{{ $form }}</pre>
</Fieldset>
</Form>
`,
options: `
<template>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="grid md:grid-cols-2 gap-4 w-full">
<div class="flex flex-col justify-center items-center gap-4">
<InputText name="username" type="text" placeholder="Username" />
<Button type="submit" severity="secondary" label="Submit" />
</div>
<Fieldset legend="Form States">
<pre>{{ $form }}</pre>
</Fieldset>
</Form>
</template>
<script>
export default {
data() {
return {
initialValues: {
username: ''
}
};
},
methods: {
resolver: ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
<\/script>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
resolver: ({ values }) => {
const errors = {};
if (!values.username) {
errors.username = [{ message: 'Username is required.' }];
}
return {
errors
};
},
onFormSubmit({ valid }) {
if (valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
</script>

View File

@ -0,0 +1,169 @@
<template>
<DocSectionText v-bind="$attrs">
<p>The <i>submit</i> callback provides an object containing the form's validity, all errors, and current states. This offers access to the form values as well as validation status and any existing errors during submission.</p>
</DocSectionText>
<div class="card flex justify-center">
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<div class="flex flex-col gap-2">
<Password name="password" placeholder="Password" :feedback="false" fluid />
<Message v-if="$form.password?.invalid" severity="error">
<ul class="mx-1 px-3">
<li v-for="(error, index) of $form.password.errors" :key="index" class="py-1">{{ error.message }}</li>
</ul>
</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</div>
<DocSectionCode :code="code" />
</template>
<script>
import { zodResolver } from '@primevue/form/resolvers';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: '',
password: ''
},
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required' }),
password: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
message: 'Password must contain at least one number.'
})
})
),
code: {
basic: `
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<div class="flex flex-col gap-2">
<Password name="password" placeholder="Password" :feedback="false" fluid />
<Message v-if="$form.password?.invalid" severity="error">
<ul class="mx-1 px-3">
<li v-for="(error, index) of $form.password.errors" :key="index" class="py-1">{{ error.message }}</li>
</ul>
</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
`,
options: `
<template>
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit" class="flex flex-col gap-4 w-full sm:w-56">
<div class="flex flex-col gap-2">
<InputText name="username" type="text" placeholder="Username" fluid />
<Message v-if="$form.username?.invalid" severity="error">{{ $form.username.errors[0]?.message }}</Message>
</div>
<div class="flex flex-col gap-2">
<Password name="password" placeholder="Password" :feedback="false" fluid />
<Message v-if="$form.password?.invalid" severity="error">
<ul class="mx-1 px-3">
<li v-for="(error, index) of $form.password.errors" :key="index" class="py-1">{{ error.message }}</li>
</ul>
</Message>
</div>
<Button type="submit" severity="secondary" label="Submit" />
</Form>
</template>
<script>
import { zodResolver } from '@primevue/form/resolvers';
import { z } from 'zod';
export default {
data() {
return {
initialValues: {
username: '',
password: ''
},
resolver: zodResolver(
z.object({
username: z.string().min(1, { message: 'Username is required' }),
password: z
.string()
.min(3, { message: 'Password must be at least 3 characters long.' })
.max(8, { message: 'Password must not exceed 8 characters.' })
.refine((value) => /[a-z]/.test(value), {
message: 'Password must contain at least one lowercase letter.'
})
.refine((value) => /[A-Z]/.test(value), {
message: 'Password must contain at least one uppercase letter.'
})
.refine((value) => /\d/.test(value), {
message: 'Password must contain at least one number.'
})
})
)
};
},
methods: {
onFormSubmit(e) {
// e.originalEvent: Represents the native form submit event.
// e.valid: A boolean that indicates whether the form is valid or not.
// e.states: Contains the current state of each form field, including validity status.
// e.errors: An object that holds any validation errors for the invalid fields in the form.
// e.values: An object containing the current values of all form fields.
if (e.valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
<\/script>
`,
composition: `
<template>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
</Card>
</template>
<script setup>
<\/script>
`
}
};
},
methods: {
onFormSubmit(e) {
// e.originalEvent: Represents the native form submit event.
// e.valid: A boolean that indicates whether the form is valid or not.
// e.states: Contains the current state of each form field, including validity status.
// e.errors: An object that holds any validation errors for the invalid fields in the form.
// e.values: An object containing the current values of all form fields.
if (e.valid) {
this.$toast.add({ severity: 'success', summary: 'Form is submitted.', life: 3000 });
}
}
}
};
</script>

View File

@ -0,0 +1,65 @@
<template>
<Form v-slot="$form" :initialValues :resolver @submit="$emit('submit', $event)" class="flex flex-col gap-4 w-full sm:w-56">
<slot v-bind="$form">
<template v-for="({ groupId, label, messages, ...rest }, name) in fields" :key="name">
<DynamicFormField :groupId :name>
<DynamicFormLabel>{{ label }}</DynamicFormLabel>
<DynamicFormControl v-bind="rest" />
<DynamicFormMessage v-for="(message, index) in messages || [{}]" :key="index" v-bind="message" />
</DynamicFormField>
</template>
<DynamicFormSubmit />
</slot>
</Form>
</template>
<script>
import { isNotEmpty } from '@primeuix/utils';
import { zodResolver } from '@primevue/form/resolvers';
import { z } from 'zod';
import DynamicFormControl from './DynamicFormControl.vue';
import DynamicFormField from './DynamicFormField.vue';
import DynamicFormLabel from './DynamicFormLabel.vue';
import DynamicFormMessage from './DynamicFormMessage.vue';
import DynamicFormSubmit from './DynamicFormSubmit.vue';
export default {
name: 'DynamicForm',
emits: ['submit'],
props: {
fields: Object
},
provide() {
return {
$fcDynamicForm: this
};
},
data() {
return {
defaultValues: {},
schemas: {}
};
},
methods: {
addField(name, schema, defaultValue) {
schema && (this.schemas[name] = schema);
this.defaultValues[name] = defaultValue;
}
},
computed: {
resolver() {
return isNotEmpty(this.schemas) ? zodResolver(z.object(this.schemas)) : undefined;
},
initialValues() {
return this.defaultValues;
}
},
components: {
DynamicFormControl,
DynamicFormField,
DynamicFormLabel,
DynamicFormMessage,
DynamicFormSubmit
}
};
</script>

View File

@ -0,0 +1,43 @@
<template>
<component :is="component" :id :name class="w-full" />
</template>
<script>
import * as PrimeVue from 'primevue/primevue';
export default {
name: 'DynamicFormControl',
props: {
as: {
type: String,
default: 'InputText'
},
schema: null,
defaultValue: {
default: ''
}
},
inject: {
$fcDynamicForm: {
default: undefined
},
$fcDynamicFormField: {
default: undefined
}
},
created() {
this.$fcDynamicForm?.addField(this.name, this.schema, this.defaultValue);
},
computed: {
id() {
return this.$fcDynamicFormField?.$props.groupId;
},
name() {
return this.$fcDynamicFormField?.$props.name;
},
component() {
return PrimeVue[this.as] ?? this.as;
}
}
};
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="flex flex-col gap-2">
<slot />
</div>
</template>
<script>
export default {
name: 'DynamicFormField',
props: {
groupId: {
type: String,
default: undefined
},
name: {
type: String,
default: undefined
}
},
provide() {
return {
$fcDynamicFormField: this
};
}
};
</script>

View File

@ -0,0 +1,21 @@
<template>
<label :for="htmlFor">
<slot />
</label>
</template>
<script>
export default {
name: 'DynamicFormLabel',
inject: {
$fcDynamicFormField: {
default: undefined
}
},
computed: {
htmlFor() {
return this.$fcDynamicFormField?.$props.groupId;
}
}
};
</script>

View File

@ -0,0 +1,54 @@
<template>
<Message v-if="visible" :severity>
<slot>{{ message }}</slot>
</Message>
</template>
<script>
import { isNotEmpty } from '@primeuix/utils';
export default {
name: 'DynamicFormLabel',
props: {
errorType: {
type: String,
default: undefined
},
severity: {
type: String,
default: 'error'
}
},
inject: {
$pcForm: {
default: undefined
},
$fcDynamicFormField: {
default: undefined
}
},
computed: {
visible() {
return this.invalid && (this.error || this.errorType === undefined);
},
message() {
return this.errorType ? this.error?.message : this.errors?.[0]?.message;
},
error() {
return this.errors?.find((error) => error.errorType === this.errorType || isNotEmpty(error[this.errorType]));
},
fieldName() {
return this.$fcDynamicFormField?.$props.name;
},
state() {
return this.$pcForm?.states?.[this.fieldName];
},
errors() {
return this.state?.errors;
},
invalid() {
return this.state?.invalid;
}
}
};
</script>

View File

@ -0,0 +1,19 @@
<template>
<Button type="submit" :severity :label />
</template>
<script>
export default {
name: 'DynamicFormSubmit',
props: {
severity: {
type: String,
default: 'secondary'
},
label: {
type: String,
default: 'Submit'
}
}
};
</script>

View File

@ -0,0 +1,37 @@
<template>
<DocPTViewer :docs="docs">
<Card style="width: 25rem">
<template #title>Advanced Card</template>
<template #subtitle>Card subtitle</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate neque
quas!
</p>
</template>
<template #footer>
<div class="flex gap-4 mt-1">
<Button label="Cancel" severity="secondary" outlined class="w-full" />
<Button label="Save" class="w-full" />
</div>
</template>
</Card>
</DocPTViewer>
</template>
<script>
import { getPTOptions } from '@/components/doc/helpers';
export default {
data() {
return {
docs: [
{
data: getPTOptions('Card'),
key: 'Card'
}
]
};
}
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="doc-main">
<div class="doc-intro">
<h1>Form Pass Through</h1>
</div>
<DocSections :docs="docs" />
</div>
<DocSectionNav :docs="docs" />
</template>
<script>
import DocApiTable from '@/components/doc/DocApiTable.vue';
import { getPTOptions } from '@/components/doc/helpers';
import PTViewer from './PTViewer.vue';
export default {
data() {
return {
docs: [
{
id: 'pt.viewer',
label: 'Viewer',
component: PTViewer
},
{
id: 'pt.doc.form',
label: 'Form PT Options',
component: DocApiTable,
data: getPTOptions('Form')
}
]
};
}
};
</script>

View File

@ -0,0 +1,8 @@
<template>
<DocSectionText v-bind="$attrs">
<p>
Visit <a href="https://github.com/primefaces/primevue-tailwind" target="_blank" rel="noopener noreferrer" class="doc-link">Tailwind Presets</a> project for detailed documentation, examples and ready-to-use presets about how to style
PrimeVue components with Tailwind CSS.
</p>
</DocSectionText>
</template>

View File

@ -0,0 +1,49 @@
<template>
<div class="doc-main">
<div class="doc-intro">
<h1>Form Theming</h1>
</div>
<DocSections :docs="docs" />
</div>
<DocSectionNav :docs="docs" />
</template>
<script>
import DocApiTable from '@/components/doc/DocApiTable.vue';
import { getStyleOptions } from '@/components/doc/helpers';
import TailwindDoc from './TailwindDoc.vue';
export default {
data() {
return {
docs: [
{
id: 'theming.styled',
label: 'Styled',
children: [
{
id: 'theming.classes',
label: 'CSS Classes',
description: 'List of class names used in the styled mode.',
component: DocApiTable,
data: getStyleOptions('Form')
}
]
},
{
id: 'theming.unstyled',
label: 'Unstyled',
description: 'Theming is implemented with the pass through properties in unstyled mode.',
children: [
{
id: 'theming.tailwind',
label: 'Tailwind',
component: TailwindDoc
}
]
}
]
};
}
};
</script>

View File

@ -0,0 +1,67 @@
<template>
<DocComponent title="Vue Form Component" header="Form" description="Form provides validation functionality and manages form state." :componentDocs="docs" :apiDocs="['Form']" :ptTabComponent="ptComponent" :themingDocs="themingDoc" />
</template>
<script>
import AccessibilityDoc from '@/doc/forms/AccessibilityDoc.vue';
import BasicDoc from '@/doc/forms/BasicDoc.vue';
import DynamicDoc from '@/doc/forms/DynamicDoc.vue';
import ImportDoc from '@/doc/forms/ImportDoc.vue';
import RegisterDoc from '@/doc/forms/RegisterDoc.vue';
import ResolversDoc from '@/doc/forms/ResolversDoc.vue';
import StatesDoc from '@/doc/forms/StatesDoc.vue';
import SubmitDoc from '@/doc/forms/SubmitDoc.vue';
import PTComponent from '@/doc/forms/pt/index.vue';
import ThemingDoc from '@/doc/forms/theming/index.vue';
export default {
data() {
return {
docs: [
{
id: 'import',
label: 'Import',
component: ImportDoc
},
{
id: 'basic',
label: 'Basic',
component: BasicDoc
},
{
id: 'states',
label: 'States',
component: StatesDoc
},
{
id: 'resolvers',
label: 'Resolvers',
component: ResolversDoc
},
{
id: 'register',
label: 'Register',
component: RegisterDoc
},
{
id: 'submit',
label: 'Submit',
component: SubmitDoc
},
{
id: 'dynamic',
label: 'Dynamic',
component: DynamicDoc
},
{
id: 'accessibility',
label: 'Accessibility',
component: AccessibilityDoc
}
],
ptComponent: PTComponent,
themingDoc: ThemingDoc
};
}
};
</script>

View File

@ -15,6 +15,6 @@
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true
}, },
"include": ["../../packages/primevue/src/**/*.d.ts", "../../packages/primevue/src/**/style/*.d.ts", "../../packages/themes/types", "../packages/themes/src/**/**/index.js"], "include": ["../../packages/primevue/src/**/*.d.ts", "../../packages/primevue/src/**/style/*.d.ts", "../../packages/themes/types", "../packages/themes/src/**/**/index.js", "../packages/form/src/**/*.d.ts"],
"exclude": ["node_modules", "**/node_modules", "**/dist"] "exclude": ["node_modules", "**/node_modules", "**/dist"]
} }

View File

@ -1,5 +1,5 @@
{ {
"$schema": "https://typedoc.org/schema.json", "$schema": "https://typedoc.org/schema.json",
"entryPoints": ["../../packages/primevue", "../../packages/themes"], "entryPoints": ["../../packages/primevue", "../../packages/themes", "../../packages/form"],
"sort": ["source-order"] "sort": ["source-order"]
} }