Skip to content

CInput

CInput — базовый примитив для построения компонентов ввода. Управляет состоянием фокуса, валидацией, aria-атрибутами и интеграцией с CForm. Сам по себе не рендерит <input> — вместо этого предоставляет данные через слот field, из которых потребитель строит собственное поле.

Когда использовать CInput напрямую?

Для большинства задач используйте CTextField. CInput нужен, когда требуется нестандартное поле: textarea с кастомным оформлением, PIN-input, числовой степпер и другие виджеты, которым нужна валидация и состояние фокуса.

Пример: кастомное поле

Verification code
Enter the 6-digit code from your email
About me0/200
Показать код
vue
<template>
  <CInput
    v-model="pin"
    id="custom-pin"
    label="PIN-код"
    :rules="pinRules"
    validate-on="blur"
  >
    <template #field="field">
      <div class="pin-wrap" :class="{ 'has-error': field.hasError }">
        <label :for="field.uid">PIN-код</label>
        <input
          v-bind="field.attrs"
          :id="field.uid"
          type="password"
          maxlength="4"
          inputmode="numeric"
          :value="pin"
          @input="(e: any) => pin = (e.target as HTMLInputElement).value"
          @focus="field.focus"
          @blur="field.blur"
        />
      </div>
    </template>
    <template #details="{ errorMessage, hasError }">
      <span :style="{ color: hasError ? 'var(--c-app-error-color)' : 'inherit' }">
        {{ errorMessage || '4-значный PIN' }}
      </span>
    </template>
  </CInput>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const pin = ref('')
const pinRules = [
  (v: string) => ({ valid: /^\d{4}$/.test(v), message: 'Введите 4-значный PIN' }),
]
</script>

Система пресетов

Пресеты — основной способ стилизации компонентов на основе CInput. Вместо условных CSS-классов в каждом шаблоне вы один раз описываете пресет и ссылаетесь на него по имени. Компонент сам применяет нужные классы под текущее состояние.

Каждое значение — массив имён утилитарных классов, поэтому пресеты работают с любым utility-first движком, который вы используете.

Зоны

Плоский пресет (ZonePreset) — это карта зон в списки классов. Зоны маппятся 1:1 на отрисованный DOM:

ЗонаЭлемент
rootобёртка .c-input
fieldкоробка .c-field (рамка / фон)
inputнативный <input>
labelплавающий лейбл
detailsстрока подсказки / ошибки
prependобёртка prepend-слота
appendобёртка append-слота

Тип CInputPreset

Пресет — это набор плоских пресетов по состояниям. base — спокойный вид, каждое состояние — отдельный полный плоский пресет:

ts
type CInputZone = 'root' | 'field' | 'input' | 'label' | 'details' | 'prepend' | 'append'
type CInputState = 'focused' | 'filled' | 'error' | 'disabled' | 'readonly'

type ZonePreset = Partial<Record<CInputZone, string[]>>

// base + опциональные плоские пресеты по состояниям — всё опционально
type CInputPreset = Partial<Record<'base' | CInputState, ZonePreset>>

Составных состояний нет и нет вложенности — состояние это просто ещё один плоский пресет.

Одно состояние за раз

Компонент всегда находится в одном текущем состоянии, и применяется пресет именно этого состояния — ничего не складывается и нет никаких приоритетов. Вы просто описываете плоский пресет на каждое состояние, активное применяется (или base, когда поле в покое). Зоны активного состояния подменяют зоны base пер-зонно; зона, которую состояние не описывает, берётся из base.

Почему одно состояние, а не стек?

Утилитарные классы — это !important с одинаковой специфичностью, поэтому при стэке конфликтующих классов (например двух bg-*) побеждает порядок в стайлшите, а не намерение. Ровно один комплект классов на зону делает результат предсказуемым.

Адресация состояния напрямую

Каждое состояние — самостоятельный плоский пресет, адресуемый как name.state. input.blue — это весь набор; input.blue.focused — только плоский пресет состояния focused.

Регистрация пресетов

Пресеты регистрируются глобально в createVuelandUI:

ts
import { createVuelandUI } from '@vueland/ui'
import type { CInputPreset } from '@vueland/ui/types'

function makePreset(color: string): CInputPreset {
  return {
    base:     { label: [color] },
    focused:  { label: [color], field: [color] },
    filled:   { label: [color] },
    error:    { label: ['text-red'], field: ['text-red'], details: ['text-red'] },
    readonly: { label: ['text-grey'] },
    // `disabled` притеняется компонентом; зона нужна только для оверрайда
  }
}

const vueland = createVuelandUI({
  presets: {
    input: {
      blue: makePreset('text-blue'),
      teal: makePreset('text-teal'),
    },
  },
})

Затем указывайте пресет по имени на любом компоненте на основе CInput:

vue
<CTextField preset="input.blue" ... />
<CTextField preset="input.teal" ... />

Распределение CInput → CField

Вы пишете один пресет. CInput его резолвит, применяет свои зоны (root, details) и раздаёт набор в поддерево поля через provide/inject. CField его инжектит и применяет field, input, label, prepend, append. Отдельный пресет для поля писать не нужно, и ничего не мутируется глобально.

Все состояния наглядно

Default
Focused
Filled
Error
Disabled
Readonly

Пример выше использует preset="input.blue" в шести состояниях:

  • Default — зоны base
  • Focusedfocused подменяет base пер-зонно
  • Filledfilled подменяет base (лейбл поднимается, цвет сохраняется)
  • Errorerror подменяет base (красный)
  • Disabled — взаимодействие заблокировано; компонент притеняет поле
  • Readonly — значение видно, но редактировать нельзя

API

Props

PropТипПо умолчаниюОписание
modelValueanyundefinedЗначение (v-model)
idstringautoБазовый ID для генерации uid, uid-label, uid-details
labelstringТекст лейбла (передаётся в слот field)
detailsstringПодсказка под полем
noDetailsbooleanfalseСкрыть блок details
clearablebooleanfalseПередать clearable в слот field
disabledbooleanfalseБлокирует фокус, добавляет aria-disabled
readonlybooleanfalseДобавляет aria-readonly, блокирует ввод
focusedbooleanfalseНачальное состояние фокуса
roleCInputRoleСемантическая роль. Управляет aria-разметкой и префиксом uid
rulesValidateFn[][]Функции валидации
validateOn'input' | 'blur''input'Момент запуска валидации
presetstringИмя пресета (dot-путь в объекте presets, переданном в createVuelandUI)

Тип CInputRole

ts
type CInputRole = 'combobox' | 'checkbox' | 'radio' | 'listbox'
ЗначениеПоведение
'combobox'Добавляет role="combobox", aria-haspopup="listbox", aria-controls, aria-expanded — для активаторов select / autocomplete
'checkbox'Привязывает aria-labelledby к лейблу
'radio'Привязывает aria-labelledby к лейблу
'listbox'Для контролов на основе listbox (aria + префикс uid)

Слоты

СлотПропсыОписание
fieldCInputFieldSlotPropsОбязательный. Рендерит само поле ввода
detailsCInputDetailsSlotPropsЗамена блока подсказки/ошибки

Пропсы слота field

ПропТипОписание
uidstringСгенерированный ID для нативного <input>
attrsRecord<string, unknown>Готовые aria-атрибуты и нативные attrs — передавать через v-bind
focusedbooleanТекущее состояние фокуса
labelstring | undefinedЗначение prop label
clearableboolean | undefinedЗначение prop clearable
disabledboolean | undefinedЗначение prop disabled
readonlyboolean | undefinedЗначение prop readonly
presetCInputPreset | undefinedРезолвнутый набор пресета (также провайдится в поддерево поля)
hasErrorbooleanЕсть ли активная ошибка
errorMessagestring | undefinedТекущее сообщение об ошибке
validatingbooleanИдёт ли async-валидация
focus() => voidВызвать при фокусе нативного элемента
blur() => voidВызвать при потере фокуса нативного элемента
reset() => voidСбросить ошибку валидации
validate() => Promise<boolean>Запустить валидацию

Пропсы слота details

ПропТипОписание
uidstringID поля
errorMessagestring | undefinedСообщение об ошибке
hasErrorbooleanЕсть ли ошибка
validatingbooleanИдёт ли async-валидация
detailsstring | undefinedЗначение prop details

События

СобытиеАргументыОписание
focusbooleanПоле получило фокус
blurПоле потеряло фокус

Expose

МетодСигнатураОписание
validate() => Promise<boolean>Запустить валидацию вручную
reset() => voidСбросить ошибку
focus() => voidПрограммно сфокусировать
blur() => voidПрограммно убрать фокус

Интеграция с CForm

CInput автоматически регистрирует свой метод validate в ближайшем родительском CForm. При вызове form.validate() все зарегистрированные поля проверяются параллельно через Promise.all.

vue
<template>
  <CForm>
    <template #default="{ validate }">
      <CInput v-model="pin" :rules="rules">
        <template #field="field">
          <input
            :id="field.uid"
            v-bind="field.attrs"
            :value="pin"
            @focus="field.focus"
            @blur="field.blur"
          />
        </template>
      </CInput>
      <button @click="validate">Проверить</button>
    </template>
  </CForm>
</template>

Автоматические aria-атрибуты

CInput формирует aria-атрибуты и передаёт их в field.attrs. Используйте v-bind="field.attrs" на нативном элементе.

АтрибутУсловие
aria-labelledby="{uid}-label"label задан, или kind = checkbox/radio
aria-labelЕсли label задан как единственная метка
aria-describedby="{uid}-details"Есть details или ошибка
aria-invalid="true"Есть ошибка валидации
aria-errormessage="{uid}-details"Есть сообщение об ошибке
aria-disabled="true"disabled = true
aria-readonly="true"readonly = true
aria-haspopup="listbox"kind = 'listbox'
aria-controls="{uid}-menu"kind = 'listbox'
aria-expandedkind = 'listbox' (обновляется при фокусе)

CSS-переменные

ПеременнаяПо умолчаниюОписание
--c-input-background-colorvar(--c-app-surface-color)Фон компонента
--c-input-primary-colorvar(--c-app-primary-color)Цвет текста в состоянии default
--c-input-error-colorvar(--c-app-error-color)Цвет текста при ошибке
--c-input-disabled-colorvar(--c-app-disabled-color)Цвет текста при disabled
--c-input-readonly-colorvar(--c-app-primary-color)Цвет текста при readonly
--c-input-readonly-bg-colorgrey lighten-4Фон поля при readonly
--c-input-field-border-radiusvar(--c-app-border-radius)Скругление поля
--c-input-details-height24pxВысота блока details

CSS-классы состояний

КлассУсловие
c-input--defaultНет ошибки, не disabled, не readonly
c-input--focusedВ фокусе
c-input--has-errorОшибка валидации
c-input--disableddisabled = true
c-input--readonlyreadonly = true
c-input--clearableclearable = true
c-input--validatingИдёт async-валидация