docs: update

This commit is contained in:
Benjamin Canac
2024-06-28 16:19:25 +02:00
parent fae627ed65
commit c9f9a248b7
23 changed files with 90 additions and 648 deletions

View File

@@ -1,94 +0,0 @@
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import type { ComponentMeta } from 'vue-component-meta'
import * as theme from '#build/ui'
const route = useRoute()
const camelName = camelCase(route.params.slug[route.params.slug.length - 1])
const name = `U${upperFirst(camelName)}`
const componentTheme = theme[camelName]
const componentMeta = await useComponentMeta(name as any)
const meta: ComputedRef<ComponentMeta> = computed(() => {
const meta = componentMeta.value.meta
if (meta.props?.length) {
meta.props = meta.props.map((prop) => {
prop.default = prop.default ?? componentTheme.defaultVariants?.[prop.name]
return prop
})
}
return meta
})
const { data: ast } = await useAsyncData(`${name}-api`, () => parseMarkdown(`
## API
${section('props')}
${section('slots')}
${section('events')}
`))
function section(type: string) {
const columns = {
props: [{ key: 'name', label: 'Prop' }, { key: 'default', label: 'Default' }, { key: 'type', addKey: 'description', label: 'Type' }],
slots: [{ key: 'name', label: 'Slot' }, { key: 'type', addKey: 'description', label: 'Type' }],
events: [{ key: 'name', label: 'Event' }, { key: 'type', addKey: 'description', label: 'Type' }]
}
const items = meta.value[type]
if (!items?.length) {
return ''
}
return `
### ${upperFirst(type)}
${table(items, columns[type])}
`
}
function table(items: any[], columns?: { key: string, addKey?: string, label: string }[], align: string = 'left') {
let table = ''
const separator = {
left: ':---',
right: '---:',
center: '---'
}
// Generate columns
const cols = columns?.length ? columns : Object.keys(items[0]).map(key => ({ key, label: upperFirst(key) }))
// Generate table headers
table += cols.map(col => col.label).join(' | ')
table += '\r\n'
// Generate table separator
table += cols.map(() => {
return separator[align] || separator.center
}).join(' | ')
table += '\r\n'
// Generate table body
items.forEach((item) => {
table += cols.map((col) => {
let code = item[col.key] ? `<code>${item[col.key].replaceAll('|', '&#124;')}</code>` : ''
if (col.addKey) {
code += item[col.addKey] ? `<p class="!mt-2">${item[col.addKey].replaceAll('\n\n', '<br>').replaceAll('\n', '<br>')}</p>` : ''
}
return code
}).join(' | ') + '\r\n'
})
return table
}
</script>
<template>
<MDCRenderer v-if="ast && (meta.props?.length || meta.slots?.length || meta.events?.length)" :body="ast.body" :data="ast.data" />
</template>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import type { ComponentMeta } from 'vue-component-meta'
const route = useRoute()
const camelName = camelCase(route.params.slug[route.params.slug.length - 1])
const name = `U${upperFirst(camelName)}`
const componentMeta = await useComponentMeta(name as any)
const meta: ComputedRef<ComponentMeta> = computed(() => componentMeta.value.meta)
const meta = await fetchComponentMeta(name as any)
</script>
<template>
@@ -25,7 +22,7 @@ const meta: ComputedRef<ComponentMeta> = computed(() => componentMeta.value.meta
</ProseTr>
</ProseThead>
<ProseTbody>
<ProseTr v-for="event in meta.events" :key="event.name">
<ProseTr v-for="event in meta.meta.events" :key="event.name">
<ProseTd>
<ProseCodeInline class="text-primary-500 dark:text-primary-400">
{{ event.name }}

View File

@@ -9,22 +9,18 @@ const camelName = camelCase(route.params.slug[route.params.slug.length - 1])
const name = `U${upperFirst(camelName)}`
const componentTheme = theme[camelName]
const componentMeta = await useComponentMeta(name as any)
const meta = await fetchComponentMeta(name as any)
const meta: ComputedRef<ComponentMeta> = computed(() => {
const meta = componentMeta.value.meta
if (meta.props?.length) {
meta.props = meta.props.map((prop) => {
prop.default = prop.default ?? componentTheme.defaultVariants?.[prop.name]
return prop
})
const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
if (!meta?.meta?.props?.length) {
return []
}
return meta
return meta.meta.props.map((prop) => {
prop.default = prop.default ?? componentTheme?.defaultVariants?.[prop.name]
return prop
})
})
console.log('meta.value', meta.value)
</script>
<template>
@@ -43,9 +39,9 @@ console.log('meta.value', meta.value)
</ProseTr>
</ProseThead>
<ProseTbody>
<ProseTr v-for="prop in meta.props" :key="prop.name">
<ProseTr v-for="prop in metaProps" :key="prop.name">
<ProseTd>
<ProseCodeInline class="text-primary-500 dark:text-primary-400">
<ProseCodeInline>
{{ prop.name }}
</ProseCodeInline>
</ProseTd>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import type { ComponentMeta } from 'vue-component-meta'
const route = useRoute()
const camelName = camelCase(route.params.slug[route.params.slug.length - 1])
const name = `U${upperFirst(camelName)}`
const componentMeta = await useComponentMeta(name as any)
const meta: ComputedRef<ComponentMeta> = computed(() => componentMeta.value.meta)
const meta = await fetchComponentMeta(name as any)
</script>
<template>
@@ -25,9 +22,9 @@ const meta: ComputedRef<ComponentMeta> = computed(() => componentMeta.value.meta
</ProseTr>
</ProseThead>
<ProseTbody>
<ProseTr v-for="slot in meta.slots" :key="slot.name">
<ProseTr v-for="slot in meta.meta.slots" :key="slot.name">
<ProseTd>
<ProseCodeInline class="text-primary-500 dark:text-primary-400">
<ProseCodeInline>
{{ slot.name }}
</ProseCodeInline>
</ProseTd>

View File

@@ -0,0 +1,32 @@
import type { ComponentMeta } from 'vue-component-meta'
const useComponentsMetaState = () => useState('components-meta', () => ({}))
export async function fetchComponentMeta(name: string): Promise<{ meta: ComponentMeta }> {
const state = useComponentsMetaState()
if (state.value[name]?.then) {
await state.value[name]
return state.value[name]
}
if (state.value[name]) {
return state.value[name]
}
// Store promise to avoid multiple calls
// add to nitro prerender
if (import.meta.server) {
const event = useRequestEvent()
event.node.res.setHeader(
'x-nitro-prerender',
[event.node.res.getHeader('x-nitro-prerender'), `/api/component-meta/${name}.json`].filter(Boolean).join(',')
)
}
state.value[name] = $fetch(`/api/component-meta/${name}.json`).then((meta) => {
state.value[name] = meta
})
await state.value[name]
return state.value[name]
}

View File

@@ -11,4 +11,12 @@ links:
## Examples
:component-api
## API
### Props
:component-props
### Slots
:component-slots

View File

@@ -21,10 +21,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -19,14 +19,6 @@ links:
:component-props
### Slots
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -20,10 +20,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -21,10 +21,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -21,10 +21,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -20,10 +20,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -20,10 +20,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -20,10 +20,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -1,4 +1,5 @@
---
title: DropdownMenu
description: Display a list of actions in a dropdown menu.
links:
- label: Radix Vue

View File

@@ -1,5 +1,5 @@
---
title: FormGroup
title: FormField
description: Display a label and additional informations around a form element.
links:
- label: GitHub
@@ -7,260 +7,6 @@ links:
to: https://github.com/nuxt/ui/blob/dev/src/runtime/components/FormGroup.vue
---
## Usage
Use the FormGroup component around an [Input](/components/input), [Textarea](/components/textarea), [Select](/components/select) or a [SelectMenu](/components/select-menu) with a `label`. The `<label>` will automatically be associated with the form element so it gets focused on click.
::component-card
---
props:
label: 'Email'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Required
Use the `required` prop to indicate that the form element is required.
::component-card
---
props:
label: 'Email'
required: true
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Description
Use the `description` prop to display a description below the label.
::component-card
---
props:
label: 'Email'
description: "We'll only use this for spam."
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Hint
Use the `hint` prop to display a hint above the form element.
::component-card
---
props:
label: 'Email'
hint: 'Optional'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Help
Use the `help` prop to display an help message below the form element.
::component-card
---
props:
label: 'Email'
help: 'We will never share your email with anyone else.'
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
### Error
Use the `error` prop to display an error message below the form element.
When used together with the `help` prop, the `error` prop will take precedence.
:component-example{component="form-group-error-example"}
::callout{icon="i-heroicons-light-bulb"}
The `error` prop will automatically set the `color` prop of the form element to `red`.
::
You can also use the `error` prop as a boolean to mark the form element as invalid.
::component-card
---
props:
label: 'Email'
error: true
excludedProps:
- ui
- error
- label
code: >-
<UInput placeholder="you@example.com" />
---
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
::callout{icon="i-heroicons-light-bulb" to="/components/form"}
Learn more about form validation in the `Form` component.
::
### Size
Use the `size` prop to change the size of the label and the form element.
::component-card
---
props:
size: 'xl'
label: 'Email'
hint: 'Optional'
description: "We'll only use this for spam."
help: 'We will never share your email with anyone else.'
excludedProps:
- label
- hint
- description
- help
code: >-
<UInput placeholder="you@example.com" icon="i-heroicons-envelope" />
---
#default
:u-input{placeholder="you@example.com" icon="i-heroicons-envelope"}
::
::callout{icon="i-heroicons-exclamation-triangle"}
This will only work with form elements that support the `size` prop.
::
### Eager Validation
By default, validation is only triggered after the initial `blur` event. This is to prevent the form from being validated as the user is typing. You can override this behavior by setting the `eager-validation` prop to `true`
:component-example{component="form-group-eager-validation-example"}
## Slots
### `label`
Use the `#label` slot to set the custom content for label.
::component-card
---
slots:
label: <UAvatar src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
---
#label
:u-avatar{src="https://avatars.githubusercontent.com/u/739984?v=4" size="3xs" class="mr-2 inline-flex"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `description`
Use the `#description` slot to set the custom content for description.
::component-card
---
slots:
description: Write only valid email address <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'
---
#description
Write only valid email address :u-icon{name="i-heroicons-information-circle" class="align-middle"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `hint`
Use the `#hint` slot to set the custom content for hint.
::component-card
---
slots:
hint: <UIcon name="i-heroicons-arrow-right-20-solid" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Step 1'
---
#hint
:u-icon{name="i-heroicons-arrow-right-20-solid"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `help`
Use the `#help` slot to set the custom content for help.
::component-card
---
slots:
help: Here are some examples <UIcon name="i-heroicons-information-circle" />
default: <UInput model-value="benjamincanac" placeholder="you@example.com" />
props:
label: 'Email'
---
#help
Here are some examples :u-icon{name="i-heroicons-information-circle" class="align-middle"}
#default
:u-input{model-value="benjamincanac" placeholder="you@example.com"}
::
### `error`
Use the `#error` slot to set the custom content for error.
::component-example
---
component: 'form-group-error-slot-example'
componentProps:
class: 'w-60'
---
::
## Usage
## Examples
@@ -275,10 +21,6 @@ componentProps:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -8,223 +8,32 @@ links:
## Usage
Use the Form component to validate form data using schema libraries such as [Yup](https://github.com/jquense/yup), [Zod](https://github.com/colinhacks/zod), [Joi](https://github.com/hapijs/joi), [Valibot](https://valibot.dev/), or your own validation logic.
It works with the [FormGroup](/components/input) component to display error messages around form elements automatically.
The form component requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from [Yup](#yup), [Zod](#zod), [Joi](#joi), or [Valibot](#valibot).
::callout{icon="i-heroicons-light-bulb"}
Note that **no validation library is included** by default, so ensure you **install the one you need**.
::
::tabs
::component-example{label="Yup"}
---
component: 'form-example-yup'
componentProps:
class: 'w-60'
---
::
::component-example{label="Zod"}
---
component: 'form-example-zod'
componentProps:
class: 'w-60'
---
::
::component-example{label="Joi"}
---
component: 'form-example-joi'
componentProps:
class: 'w-60'
---
::
::component-example{label="Valibot"}
---
component: 'form-example-valibot'
componentProps:
class: 'w-60'
---
::
::
## Custom validation
Use the `validate` prop to apply your own validation logic.
The validation function must return a list of errors with the following attributes:
- `message` - Error message for display.
- `path` - Path to the form element corresponding to the `name` attribute.
::callout{icon="i-heroicons-light-bulb"}
Note: this can be used alongside the `schema` prop to handle complex use cases.
::
::component-example
---
component: 'form-example-basic'
componentProps:
class: 'w-60'
---
::
This can also be used to integrate with other validation libraries. Here is an example with [Vuelidate](https://github.com/vuelidate/vuelidate):
```vue
<script setup lang="ts">
import useVuelidate from '@vuelidate/core'
const props = defineProps({
rules: { type: Object, required: true },
model: { type: Object, required: true }
})
const form = ref();
const v = useVuelidate(props.rules, props.model)
async function validateWithVuelidate() {
v.value.$touch()
await v.value.$validate()
return v.value.$errors.map((error) => ({
message: error.$message,
path: error.$propertyPath,
}))
}
defineExpose({
validate: async () => {
await form.value.validate()
}
})
</script>
<template>
<UForm ref="form" :model="model" :validate="validateWithVuelidate">
<slot />
</UForm>
</template>
```
## Backend validation
You can manually set errors after form submission if required. To do this, simply use the `form.setErrors` function to set the errors as needed.
```vue
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '#ui/types'
const state = reactive({
email: undefined,
password: undefined
})
const form = ref()
async function onSubmit (event: FormSubmitEvent<any>) {
form.value.clear()
try {
const response = await $fetch('...')
// ...
} catch (err) {
if (err.statusCode === 422) {
form.value.setErrors(err.data.errors.map((err) => ({
// Map validation errors to { path: string, message: string }
message: err.message,
path: err.path,
})))
}
}
}
</script>
<template>
<UForm ref="form" :state="state" @submit="onSubmit">
<UFormGroup label="Email" name="email">
<UInput v-model="state.email" />
</UFormGroup>
<UFormGroup label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormGroup>
<UButton type="submit">
Submit
</UButton>
</UForm>
</template>
```
## Input events
The Form component automatically triggers validation upon `submit`, `input`, `blur` or `change` events.
This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the `validate-on` prop.
::callout{icon="i-heroicons-light-bulb"}
Note that the `input` event is not triggered until after the initial `blur` event. This is to prevent the form from being validated as the user is typing. You can override this behavior by setting the [`eager-validation`](/components/form-group#eager-validation) prop on [`FormGroup`](/components/form-group) to `true`.
::
::component-example
---
component: 'form-example-elements'
componentProps:
class: 'w-60'
hiddenCode: true
---
::
::callout{icon="i-simple-icons-github" to="https://github.com/nuxt/ui/blob/dev/docs/components/content/examples/FormExampleElements.vue" target="_blank"}
Take a look at the component!
::
## Error event
You can listen to the `@error` event to handle errors. This event is triggered when the form is validated and contains an array of `FormError` objects with the following fields:
- `id` - the identifier of the form element.
- `path` - the path to the form element matching the `name`.
- `message` - the error message to display.
Here is an example of how to focus the first form element with an error:
::component-example
---
component: 'form-example-on-error'
componentProps:
class: 'w-60'
---
::
## Props
:component-props
## Examples
## API
### Props
:component-props
### Slots
:component-slots
### Events
:component-events
### Exposed
When accessing the component via a template ref, you can use the following:
::field-group
::field{name="submit ()" type="Promise<void>"}
Triggers form submission.
::
::field{name="validate (path?: string | string[], opts: { silent?: boolean })" type="Promise<T>"}
Triggers form validation. Will raise any errors unless `opts.silent` is set to true.
::
::field{name="clear (path?: string)"}
Clears form errors associated with a specific path. If no path is provided, clears all form errors.
::
::field{name="getErrors (path?: string)" type="FormError[]"}
Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.
::
::field{name="setErrors (errors: FormError[], path?: string)"}
Sets form errors for a given path. If no path is provided, overrides all errors.
::
::field{name="errors" type="Ref<FormError[]>"}
A reference to the array containing validation errors. Use this to access or manipulate the error information.
::
::
| Name | Type |
| ---- | ---- |
| `submit()` | `Promise<void>` <br> [Triggers form submission.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `validate(path?: string \| string[], opts: { silent?: boolean })` | `Promise<T>` <br> [Triggers form validation. Will raise any errors unless `opts.silent` is set to true.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `clear(path?: string)` | `void` <br> [Clears form errors associated with a specific path. If no path is provided, clears all form errors.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `getErrors(path?: string)` | `FormError[]` <br> [Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `setErrors(errors: FormError[], path?: string)` | `void` <br> [Sets form errors for a given path. If no path is provided, overrides all errors.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `errors` | `Ref<FormError[]>` <br> [A reference to the array containing validation errors. Use this to access or manipulate the error information.]{class="text-gray-600 dark:text-gray-300 mt-1"} |
| `disabled` | `Ref<boolean>` |

View File

@@ -23,10 +23,6 @@ navigation:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -34,6 +34,7 @@ It also renders an `<a>` tag when a `to` prop is provided, otherwise it defaults
It is used underneath by the [Button](/components/button), [Dropdown](/components/dropdown) and [VerticalNavigation](/components/vertical-navigation) components.
## IntelliSense
If you're using VSCode and wish to get autocompletion for the classes `active-class` and `inactive-class`, you can add the following settings to your `.vscode/settings.json`:
```json [.vscode/settings.json]
@@ -45,6 +46,12 @@ If you're using VSCode and wish to get autocompletion for the classes `active-cl
}
```
## Props
## API
### Props
:component-props
### Slots
:component-slots

View File

@@ -23,10 +23,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -20,10 +20,6 @@ links:
:component-slots
### Events
:component-events
## Theme
:component-theme

View File

@@ -19,10 +19,6 @@ links:
:component-props
### Slots
:component-slots
### Events
:component-events

View File

@@ -82,7 +82,6 @@ export default defineNuxtConfig({
'/components': { redirect: '/components/app', prerender: false }
},
componentMeta: {
debug: 2,
exclude: [
'@nuxt/content',
'@nuxt/icon',