← Курс/Template Refs: прямой доступ к DOM#210 из 257+20 XP

Template Refs: прямой доступ к DOM

Зачем нужен прямой доступ к DOM

Vue поощряет декларативный подход — вы описываете состояние, и Vue сам обновляет DOM. Но иногда нужно обратиться к DOM-элементу напрямую:

  • поставить фокус на поле ввода
  • запустить анимацию через стороннюю библиотеку (GSAP, three.js)
  • измерить размеры элемента
  • взаимодействовать с нативными видео/аудио API
  • Для таких случаев Vue предоставляет **template refs**.

    Синтаксис: ref="name"

    В шаблоне добавьте атрибут ref с именем:

    <template>
      <input ref="inputEl" type="text" placeholder="Введите текст">
      <button @click="focusInput">Поставить фокус</button>
    </template>
    
    <script setup>
    import { ref, onMounted } from 'vue'
    
    const inputEl = ref(null)  // имя переменной совпадает с ref="inputEl"
    
    onMounted(() => {
      // DOM доступен только после монтирования компонента
      inputEl.value.focus()
    })
    
    function focusInput() {
      inputEl.value.focus()
    }
    </script>

    useTemplateRef() — новый API Vue 3.5+

    Vue 3.5 добавил более явный способ через useTemplateRef():

    <template>
      <input ref="myInput">
    </template>
    
    <script setup>
    import { useTemplateRef, onMounted } from 'vue'
    
    const inputEl = useTemplateRef('myInput')
    
    onMounted(() => {
      console.log(inputEl.value)  // HTMLInputElement
    })
    </script>

    Рефы на компоненты

    Если ref указан на дочернем компоненте, вы получаете доступ к его публичному интерфейсу. В Composition API с <script setup> компонент по умолчанию закрыт — нужно явно использовать defineExpose:

    <!-- ChildComponent.vue -->
    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    
    // Явно экспортируем то, что хотим сделать доступным
    defineExpose({ count, increment: () => count.value++ })
    </script>
    <!-- ParentComponent.vue -->
    <template>
      <ChildComponent ref="childRef" />
      <button @click="childRef.increment()">Увеличить в дочернем</button>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    const childRef = ref(null)
    </script>

    v-for и рефы

    Если ref используется в v-for, переменная получит массив DOM-элементов:

    <template>
      <li v-for="item in items" :key="item.id" ref="listItems">
        {{ item.name }}
      </li>
    </template>
    
    <script setup>
    const listItems = ref([])  // будет массивом li-элементов
    </script>

    Когда использовать template refs

    Используй refs когда:

  • нужен императивный доступ к DOM API (focus, scroll, play)
  • интегрируешь сторонние библиотеки, которые управляют DOM сами
  • нужно измерить размеры/позицию элемента
  • Не используй refs для:

  • чтения/записи данных (для этого есть реактивное состояние)
  • условного рендеринга (для этого v-if/v-show)
  • любой логики, которую можно выразить декларативно
  • Помни: ref.value равен null до монтирования компонента. Обращайся к нему в onMounted или позже.

    Примеры

    Эмуляция template refs: регистрация DOM-элементов по имени и доступ к ним после монтирования

    // Эмулируем систему template refs Vue — регистрацию и доступ к DOM-элементам.
    
    class ComponentInstance {
      constructor(name) {
        this.name = name
        this._refs = {}
        this._mounted = false
        this._mountCallbacks = []
      }
    
      // Аналог ref="name" в шаблоне — регистрирует элемент
      registerRef(name, element) {
        this._refs[name] = element
        console.log(`  [ref] "${name}" зарегистрирован: <${element.tagName}>`)
      }
    
      // Аналог onMounted — вызывается после рендера
      onMounted(callback) {
        if (this._mounted) {
          callback()
        } else {
          this._mountCallbacks.push(callback)
        }
      }
    
      // Симуляция процесса монтирования
      mount() {
        console.log(`\nМонтирование компонента "${this.name}"...`)
        this._mounted = true
        this._mountCallbacks.forEach(cb => cb())
        this._mountCallbacks = []
      }
    
      // Получить ref по имени (как inputEl.value в Vue)
      getRef(name) {
        if (!this._mounted) {
          console.warn(`  ПРЕДУПРЕЖДЕНИЕ: Обращение к ref "${name}" до монтирования — null!`)
          return null
        }
        return this._refs[name] || null
      }
    }
    
    // Псевдо-DOM элементы
    const pseudoInput = {
      tagName: 'INPUT',
      value: '',
      focused: false,
      focus() {
        this.focused = true
        console.log('  [DOM] input.focus() вызван — поле в фокусе')
      }
    }
    
    const pseudoVideo = {
      tagName: 'VIDEO',
      playing: false,
      play() {
        this.playing = true
        console.log('  [DOM] video.play() вызван — видео запущено')
      },
      pause() {
        this.playing = false
        console.log('  [DOM] video.pause() вызван — видео остановлено')
      }
    }
    
    // Создаём компонент
    const comp = new ComponentInstance('FormComponent')
    
    // Попытка обратиться к ref ДО монтирования
    console.log('=== До монтирования ===')
    comp.getRef('inputEl')  // вернёт null с предупреждением
    
    // Регистрируем рефы (как при рендере шаблона)
    comp.registerRef('inputEl', pseudoInput)
    comp.registerRef('videoEl', pseudoVideo)
    
    // Регистрируем колбэк onMounted
    comp.onMounted(() => {
      console.log('\n  [onMounted] Компонент смонтирован! Ставим фокус...')
      const input = comp.getRef('inputEl')
      if (input) input.focus()
    })
    
    // Монтируем
    comp.mount()
    
    // Теперь можно работать с рефами
    console.log('\n=== Работа с рефами после монтирования ===')
    const video = comp.getRef('videoEl')
    video.play()
    console.log('  Видео играет:', video.playing)
    video.pause()
    console.log('  Видео играет:', video.playing)
    
    console.log('\n=== Итог ===')
    console.log('inputEl.focused:', comp.getRef('inputEl').focused)
    console.log('videoEl.playing:', comp.getRef('videoEl').playing)