Vue3可媲美Element Plus Tree组件开发之append节点

在前面的章节,我们完成了可媲美Element Plus Tree组件的基本开发。通过实现各种计算属性,tree数据状态变化引起的视图更新被计算属性所接管了,无需我们再手动做各种遍历、查找以及手动监听操作,这样后续开发高级功能变得易如反掌啦!!

在这里插入图片描述

看下提供给用户的vitepress文档说明:

在这里插入图片描述

在这里插入图片描述

操作演示:

在这里插入图片描述

前面我们实现了几个计算属性:

  • index

    节点在扁平化列表中的位置索引

  • length

    父节点的所有子孙节点的长度

  • visibleLength

    可见子孙节点的长度

  • lineLength

    参照线的长度

这些计算属性在新增一个节点,尤其是子节点时都会被影响到,触发重新计算以保证前面实现的基本功能是完好的。而无需我们在实现新增节点时再去兼顾基础功能,这就是Vue3 composition api的计算属性的魅力,让复杂的功能变得简单,组件的开发者只需要把关注点放到影响计算属性变化的数据上即可,Life is so easy!

新增类型、接口

定义ts的类型和接口,注意给用户提供的接口一定要遵循“迪米特法则”。

在这里插入图片描述

核心插入逻辑

/**
 * 新增顶级节点
 * @param child 要新增的叶子节点
 * @param data 扁平化节点列表
 * @param treeData 结构化节点树
 * @param optionProps 组件配置选项
 */
export function appendTop(child: ILeafNode, data: IFlatTreeNode[], treeData: ITreeNode[], optionProps: OptionProps) {
  // 节点id命名逻辑:如果指定了就用用户指定的,否则按照列表长度生成
  child.id = child.id || 'id-' + (data.length + 1)
  // 从新增节点拷贝数据作为original child node
  const ocNode = { ...child }
  // 扁平化new child node
  const ncNode = {
    ...child,
    level: 1,
    isLeaf: true,
    originalNode: ocNode
  } as IFlatTreeNode
  // 要插入的位置为列表最后
  const insertIndex = data.length
  // 绑定新插入的扁平化节点的前置节点
  ncNode.prev = data[data.length - 1]
  // 对新的扁平化节点进行初始化
  initFlatTreeNode(ncNode, optionProps)
  // 原始树结构中新增节点,注意!!操作的是响应式数据
  ref(treeData).value.push(ocNode as never)
  // 扁平化列表中插入新节点
  ref(data).value.splice(insertIndex, 0, ncNode as never)
}

function initFlatTreeNode(node: IFlatTreeNode, optionProps: OptionProps) {
  ...

  /**
   * 给扁平化节点绑定新增子节点的方法
   * @param child 新增的子节点
   * @param data 扁平化列表
   */
  node.append = (child: ILeafNode, data: IFlatTreeNode[]) => {
    // 同新增一级节点
    child.id = child.id || 'id-' + (data.length + 1)
    // 当前节点原始节点
    const oNode = node.originalNode
    // 新增节点原始节点
    const ocNode = { ...child }
    // 新增节点扁平化节点
    const ncNode = {
      ...child,
      parent: node, // 绑定父节点
      level: node.level + 1,
      isLeaf: true,
      originalNode: ocNode
    } as IFlatTreeNode
    // 计算插入位置
    const insertIndex = calcInsertIndex(node)
    // 插入到最后的情况下,设置前置节点
    if (insertIndex === data.length) {
      ncNode.prev = data[data.length - 1]
    } else {
      // 插入到中间,绑定prev的逻辑,把prev链接起来
      const next = data[insertIndex]
      ncNode.prev = next.prev
      // 注意操作的是响应式对象,以确保可以触发index属性重新计算!!
      ref(next).value.prev = ncNode as never
    }
    // 初始化扁平化节点
    initFlatTreeNode(ncNode, optionProps)
    // 通过响应式对象获取其操作对象
    const oNodeVal = ref(oNode).value
    const nodeVal = ref(node).value
    const childrenName = optionProps.childrenName as 'children'
    // 对原先的叶子节点进行设置和初始化,变为非叶子节点
    if (!oNodeVal[childrenName]) {
      oNodeVal[childrenName] = []
      initParentNode(oNode, optionProps)
      nodeVal.isLeaf = false
    }
    // 插入到原始结构化节点
    oNodeVal[childrenName].push(ocNode as never)
    // 所在的节点将其展开(如果折叠的话)
    oNodeVal.expanded = true
    nodeVal.expanded = true
    // 插入到扁平化节点列表
    ref(data).value.splice(insertIndex, 0, ncNode as never)
  }
}

/**
 * 插入子节点位置逻辑:如果是叶节点,则为下一个位置,否则要加上子一代节点的长度
 * @param node
 */
function calcInsertIndex(node: IFlatTreeNode): number {
  return node.index.value + 1 + (node.isLeaf ? 0 : node.originalNode.length!.value)
}

Tree组件模板调整

原先给icon插槽传入的节点参数,不符合迪米特法则,暴露了内部操作属性和方法,规范的做法是拷贝一个副本!!只给用户提供其关心的几个属性,调整为:

在这里插入图片描述

对于一级节点新增操作,我们将对tree组件expose一个可操作的对象,为此把这个对象中要定义的方法抽取到ts接口中,以方便客户端API的使用:

// Tree组件对外导出的方法定义
export interface ExposeProps {
  appendTop: (newNode: ILeafNode) => void
}

导出逻辑:

在这里插入图片描述

而针对节点的操作,给用户提供的API,包装一个函数来返回要操作的接口:

// 返回节点操作方法的函数
const nodeOperation = (node: IFlatTreeNode): ITreeNodeOperation => {
  // 注意,这里不应该直接给用户提供node,而是要包成对外公开的ITreeNodeOperation,遵循迪米特法则!!
  return {
    append(newNode: ILeafNode) {
      node.append(newNode, originalFlatData)
    }
  }
}

对应的插槽实现的地方:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java小卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值