# 表单布局

## 使用方法

`FormLayout` 排列字段。它不是 `<form>`。字段放在默认插槽里，再选一个方向。

```vue
<script setup lang="ts">
import { FormLayout } from '@astryx-vue/core/FormLayout'
import { TextInput } from '@astryx-vue/core/TextInput'
</script>

<template>
  <FormLayout>
    <TextInput v-model="name" label="姓名" />
    <TextInput v-model="email" label="邮箱" />
  </FormLayout>
</template>
```

`direction` 取 `vertical`（默认）、`horizontal` 或 `horizontal-labels`。纵向按间距刻度 4 往下排。横向把字段放进等宽的列。横向标签把标签放在左边、控件放在右边，窄于 480px 时改回纵向。

`defaultOptionality` 取 `optional` 或 `required`。它只影响没有自己写 `isOptional` 或 `isRequired` 的字段的标记和 `aria-required`，不写原生 `required`。同一字段两个都写时，选填优先。没有 `FormLayout` 祖先时，`aria-required` 仍是 `isRequired && !isOptional`。

嵌套的布局各自保留方向。纵向表单里可以放一行横向字段。

### 结构

- **表单标题**: 说明这个表单是干什么的标题。
- **字段**: 带标签、用来收集数据的输入组件。
- **页脚**: 装着提交或取消这类确认按钮。

## 最佳实践

| 指引 | 实践 |
| --- | --- |
| 推荐 | 一组短字段，例如名和姓，用横向。 |
| 推荐 | 每个字段都有标签、而且表单比手机宽时，用横向标签。 |
| 避免 | 把页面标题这类无关内容放进布局。它会参加网格排列。 |
| 避免 | 指望 `defaultOptionality` 拦住提交。它只改标记和 `aria-required`。 |

## 示例

### 横向

两个字段并排，适合姓和名这类成对输入。

```vue
<script setup lang="ts">
import { FormLayout } from '@astryx-vue/core/FormLayout'
import { TextInput } from '@astryx-vue/core/TextInput'
import { shallowRef } from 'vue'

const first = shallowRef('Jordan')
const last = shallowRef('Rivera')
</script>

<template>
  <FormLayout direction="horizontal" style="width: 100%; max-width: 400px">
    <TextInput v-model="first" label="First Name" />
    <TextInput v-model="last" label="Last Name" />
  </FormLayout>
</template>
```

### 设置表单

设置表单把标签放在输入框旁边。

```vue
<script setup lang="ts">
import { FormLayout } from '@astryx-vue/core/FormLayout'
import { Selector } from '@astryx-vue/core/Selector'
import { TextArea } from '@astryx-vue/core/TextArea'
import { TextInput } from '@astryx-vue/core/TextInput'
import { shallowRef } from 'vue'

const displayName = shallowRef('Jane Doe')
const email = shallowRef('jane@example.com')
const timezone = shallowRef('America/Los_Angeles')
const bio = shallowRef('')

const timezones = [
  { label: 'Pacific Time', value: 'America/Los_Angeles' },
  { label: 'Eastern Time', value: 'America/New_York' },
  { label: 'UTC', value: 'UTC' },
]
</script>

<template>
  <FormLayout direction="horizontal-labels">
    <TextInput v-model="displayName" label="Display Name" />
    <TextInput v-model="email" label="Email" />
    <Selector v-model="timezone" label="Timezone" :options="timezones" />
    <TextArea v-model="bio" label="Bio" :rows="3" />
  </FormLayout>
</template>
```

### 混合控件

同一张表单里有文本框、选择器和复选框。

```vue
<script setup lang="ts">
import { CheckboxList, CheckboxListItem } from '@astryx-vue/core/CheckboxList'
import { FormLayout } from '@astryx-vue/core/FormLayout'
import { Selector } from '@astryx-vue/core/Selector'
import { TextInput } from '@astryx-vue/core/TextInput'
import { shallowRef } from 'vue'

const name = shallowRef('Maya Torres')
const role = shallowRef('editor')
const notifications = shallowRef(['email', 'push'])

const roles = [
  { label: 'Viewer', value: 'viewer' },
  { label: 'Editor', value: 'editor' },
  { label: 'Admin', value: 'admin' },
]
</script>

<template>
  <FormLayout>
    <TextInput v-model="name" label="Full Name" />
    <Selector v-model="role" label="Role" :options="roles" />
    <CheckboxList v-model="notifications" label="Notifications">
      <CheckboxListItem label="Email" value="email" />
      <CheckboxListItem label="SMS" value="sms" />
      <CheckboxListItem label="Push" value="push" />
    </CheckboxList>
  </FormLayout>
</template>
```

### 嵌套地址

地址表单把纵向排列和横向成组的字段混在一起。

```vue
<script setup lang="ts">
import { FormLayout } from '@astryx-vue/core/FormLayout'
import { TextInput } from '@astryx-vue/core/TextInput'
import { shallowRef } from 'vue'

const first = shallowRef('Priya')
const last = shallowRef('Sharma')
const email = shallowRef('priya.sharma@example.com')
const city = shallowRef('San Francisco')
const state = shallowRef('CA')
const zip = shallowRef('94105')
</script>

<template>
  <FormLayout>
    <FormLayout direction="horizontal">
      <TextInput v-model="first" label="First Name" />
      <TextInput v-model="last" label="Last Name" />
    </FormLayout>
    <TextInput v-model="email" label="Email" />
    <FormLayout direction="horizontal">
      <TextInput v-model="city" label="City" />
      <TextInput v-model="state" label="State" />
      <TextInput v-model="zip" label="ZIP" />
    </FormLayout>
  </FormLayout>
</template>
```

## Form Layout Props

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `direction` | `'vertical' \| 'horizontal' \| 'horizontal-labels'` | `'vertical'` | 字段怎么排。`horizontal` 是等宽的列。`horizontal-labels` 把标签放在左边，窄于 480px 时改成纵向。 |
| `defaultOptionality` | `'optional' \| 'required'` | - | 没有自己写 `isOptional` 或 `isRequired` 的字段用这个默认值。它改标记和 `aria-required`，不写原生 `required`。两个都写时，选填优先。 |

## 插槽

| 名称 | 说明 |
| --- | --- |
| `default` | 字段。嵌套的 `FormLayout` 保留自己的方向。 |

## 原生属性

`FormLayout` 渲染的是 `<div>`，不是 `<form>`。

以下内容落到这个元素上：

- HTML 属性。
- `aria-*` 属性。
- `data-*` 属性。
- `class`。
- `style`。

`class` 会合并。根元素写 `data-direction`。
