模板引用

@Template Refs
HTML 原生开发中,经常需要获取元素|节点,并进一步处理;更多信息,请访问 HTML - DOM
早期流行的 JQuery 也是通过操作 DOM 实现
Vue 中更多的是通过数据驱动的方式实现交互,但有时候避免不了仍然需要操作 DOM
通过 ref 标识获取/引用 DOM 节点或组件实例,如本地或第三方组件库
需引入包 useTemplateRef 创建浅层 ref;其值将与模板中的具有匹配 ref attribute 的元素或组件同步
早期版本使用 ref 包;注意两者使用方式的区别
更多使用,请访问 模板引用 - Template RefsAPI - ref
import { useTemplateRef } from 'vue'
Application
焦点管理:当需要自动聚焦到输入框或其它表单元素时
与第三方库交互:例如集成某些需要直接 DOM 节点的 JavaScript 库(如Chart.js)
执行动画:当需要对元素执行复杂的动画效果,而这些效果难以通过 CSS 实现时
文件上传:触发文件选择器等原生控件的操作
测量尺寸:获取元素的宽度、高度等信息用于布局调整
直接获取会失败,应在组件 挂载完毕 onMounted 再获取
使用时,应判断是否为空
该属性并不显示在结构中,F12 检查元素时,不可见
获取普通 DOM 节点
拿到 DOM 节点,就拿到了该元素的所有信息
<div ref="dom" 
  class="base active"  
  style="color: #f40;padding: 10px;"
  data-ind="100" >
    hi,there 
    <span>88</span>
</div>
import { useTemplateRef, onMounted } from 'vue'
let dom = useTemplateRef('dom')
onMounted(() => {
  console.log(dom);
  console.log(dom.value);
  console.log(dom.value.innerText);
  console.log('nodes', dom.value.childNodes[0]);
  console.log('children', dom.value.children[0]);
  console.log('classList', dom.value.classList);
  console.log('className', dom.value.className);
  console.log('style', dom.value.style);
  console.log('getAttribute', dom.value.getAttribute('data-ind'));
  console.log('getAttribute', dom.value.getAttribute('style'));
  console.log('getAttribute', dom.value.getAttribute('class'));
})
[] 焦点管理 - 页面加载时自动聚焦;注册、登录的第一个表单域
方案1:使用表单属性 autofocus
<input type="text" ref="ipt" placeholder="your name" autofocus>
方案2:使用模板引用
<div class="home">
  <input type="text" ref="ipt" placeholder="your name">
  // Other
</div>
import { useTemplateRef, onMounted } from 'vue';
const ipt = useTemplateRef('ipt')
onMounted(() => {
  ipt.value.focus()
})
[] 焦点管理 - 单击搜索按钮打开搜索框并自动聚焦
<div class="home">
  <input type="text" ref="ipt">
</div>
<button @click="focus">click for focus</button>
import { useTemplateRef, onMounted } from 'vue';
const ipt = useTemplateRef('ipt')
const focus = () => ipt.value.focus()
onMounted(() => {
  ipt.value.focus()
})
获取组件
返回组件实例
默认情况下,组件内部的属性和方法对外部不公开,所以获取到的 ref 仅仅是一个代理对象,看不到组件的内部细节
组件需要显示的暴露属性或方法 defineExpose
defineExpose 应该写在暴露的对象后面
<template>
    <div @click="getTitle">only for test - {{ title }}</div>
</template>
    
<script setup>
    import { ref } from 'vue';
    let title = ref(18)
    function getTitle() {
        console.log(title.value);
    }
    defineExpose({
        title,
        getTitle
    })
</script>
refs can be useful in certain situations where direct access to a child component's internal state or methods is necessary.
尽量避免通过 ref 引用组件,请使用组件的通信机制 props 和emit;详见后续组件内容
Remember that accessing a child component's data directly using refs can make your components tightly coupled. Consider using props and events for communication between components
[] 单击打开弹窗;更多信息,请访问 实操 - Modal
父组件获取子组件|弹窗组件实例,执行弹窗的方法
子组件|弹窗组件定义方法并暴露;方法利用 bool 将弹窗组件的隐藏状态变为显示
多ref
列表渲染中指定 ref,将获取多个元素
必须声明为数组 [] 形式,否则无法遍历
不建议每个子节点都标注 ref 属性
建议获取父节点,再根据节点关系获取子节点
<div v-for="(item,index) in 10" ref="navRef">{{item}}</div>
const navRef = useTemplateRef('navRef')
onMounted(() => {
    navRef.value.forEach(item => {
    console.log(item.innerHTML);
    })
})
Summary & Homework
Summary
用于获取 DOM 节点实例 或 组件实例
需引入 ref/useTemplateRef
使用时,应判定元素是否就绪
Homework
完成上述焦点管理案例
[] 页面滚动控制
变换子标题样式,如添加阴影
<header> 固定在顶部 fixed
为 <header> 指定 ref,在页面加载时使用,并监听页面的滚动事件
页面卸载时,移除事件监听;更多生命周期函数的使用,请访问 Lifecycle
<header class="header" ref="header">header</header>
import { onMounted, onUnmounted, useTemplateRef } from 'vue'

const header = useTemplateRef('header')

const fn = () => {
  header.value.classList.toggle('fixed', window.scrollY > 1000)
}

onMounted(() => {
  window.addEventListener('scroll', fn)
})

onUnmounted(() => {
  window.removeEventListener('scroll', fn)
})
[] 页面滚动控制 - HomeView.vue → 子标题
页面/视图滚动到某个位置首页 - 标题页 tab-box 粘性定位在顶部 sticky
利用 getBoundingClientRect() 获取元素相对视口的 top;当 top 为0时,表示粘性定位触发,应用特定样式
这里使用传统事件方式
<div class="tab-box" ref="tab-box"></div>
import { onMounted, onUnmounted, useTemplateRef } from 'vue'

const tabBox = useTemplateRef('tab-box')

onMounted(() => {
  window.onscroll = function () {
    const rect = tabBox.value.getBoundingClientRect()
    if (rect.top <= 0) {
      tabBox.value.classList.add('sticky')
    } else {
      tabBox.value.classList.remove('sticky')
    }
  }
})

onUnmounted(() => {
  window.onscroll = null
})

[] 使用事件监听 addEventListener() 和 样式的 toggle() 优化

onMounted(() => {
  window.addEventListener('scroll', () => {
    let rect = tabBox.value.getBoundingClientRect()
    tabBox.value.classList.toggle('sticky', rect.top <= 0)
  })
})