Vue.Draggable 心得

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue.Draggable 心得相关的知识,希望对你有一定的参考价值。

Vue.Draggable 为基于 Sortable.js 的 Vue 组件,用以实现拖拽功能。

一般用法

  • Vue.Draggable 当界面拖动的时候 data 也同时在变化
  • Vue.Draggable 新版本废弃了 options 属性,建议使用 v-bind 属性作为配置项

html vs. Render VNode

一开始,我用标签的方法,基本可以实现拖放,每处要拖放的地方,在子元素里面加多一层 <draggable /> 即可。因为组件嵌套组件,肯定涉及递归的概念。组件还好可以封装为 Vue 组件,从而实现递归,例如这个:

<template>
  <!--被渲染的组件-->
  <span>
    <Input v-if="item.type === 'input_text'" @click.native.stop="activeSection($event, mainData.item)" @mouseenter.native.stop="showHoverBorder($event, item)" @mouseleave.native.stop="hideHoverBorder" :data-uid="item.uid" type="text" class="input" />

    <Input @click.native="activeSection($event, item)" @mouseenter.native.stop="showHoverBorder($event, item)" :data-uid="item.uid" type="password" class="input" password v-if="item.type === 'input_password'" />
    <Input @click.native="activeSection($event, item)" @mouseenter.native.stop="showHoverBorder($event, item)" :data-uid="item.uid" type="textarea" class="input" :rows="5" v-if="item.type === 'input_textarea'" />
  </span>
</template>

<script lang="ts">
import SelectionMixins from "./selection-mixins";

export default 
  mixins: [SelectionMixins],
  props: 
    metaData:  required: false ,
    mainCmp: 
      required: false,
      default() 
        // 不知为何传不了进来
        // @ts-ignore
        return window.DESIGNER_MAIN_CMP;
      ,
    ,
    mainData: Object,
    item:  type: Object, required: true ,
    isWysiwyg:  type: Boolean, required: false, default: true ,
  ,
  methods: ,
;
</script>

<style lang="less" scoped>
.input 
  width: 400px;
  margin-right: 10px;

</style>

然后里面调用。但是,对于布局类的嵌套,标签无法实现递归……

  <div v-for="(item, index) in metaData" :key="item.type +'_'+ index">
    <!-- 表单布局 -->
    <Form v-if="item.type == 'Form'" :label-width="form_cfg.labelWidth" class="widget-form" :label-position="form_cfg.labelPosition" :data-uid="item.uid" @mouseenter.native.stop="showHoverBorder($event, item)" @mouseleave.native.stop="hideHoverBorder" @click.native.stop="activeSection($event, item)">

      <draggable v-model="item.children" group="FormItem" chosenClass="chosen" forceFallback="true" animation="1000" @end="activeCmp=null;">

        <FormItem v-for="(formItem, formItem_index) in item.children" :key="formItem.type +'_'+ formItem_index" :label="formItem.label" @mouseenter.native.stop="showHoverBorder($event, formItem)" @mouseleave.native.stop="hideHoverBorder" @click.native.stop="activeSection($event, formItem)" :data-uid="formItem.uid">

          <draggable tag="span" v-model="formItem.children" group="FormItemWidget" chosenClass="chosen" forceFallback="true" animation="1000" @add="onDrapNewAdd" @end="activeCmp=null;">

            <span v-for="(formEl, formEl_index) in formItem.children" :key="formEl.type +'_'+ formEl_index">

              <!-- 插入栅格 -->
              <Row v-if="formEl.type === 'Row'" class="column-container" :data-uid="formEl.uid" @click.native.stop="activeSection($event, formEl)" @mouseenter.native.stop="showHoverBorder($event, formEl)" @mouseleave.native.stop="hideHoverBorder">

                <draggable tag="Col" v-model="formEl.children" group="FormItem" chosenClass="chosen" forceFallback="true" animation="500" v-for="(ColItem, ColItem_index) in formEl.children" :key="ColItem.type +'_'+ ColItem_index" :component-data="getColSpan(formEl.children)" :data-uid="ColItem.uid" @start="activeCmp=null;" @click.native.stop="activeSection($event, ColItem)" @mouseenter.native.stop="showHoverBorder($event, ColItem)" @mouseleave.native.stop="hideHoverBorder">
                  <RenderCmp v-for="(ColItemWidget, ColItemWidget_index) in ColItem.children" :key="ColItemWidget.type +'_'+ ColItemWidget_index" :item="ColItemWidget" :main-cmp="mainCmp" :meta-data="metaData" :is-wysiwyg="true" />
                </draggable>

                <draggable tag="Col" v-if="!formEl.children || !formEl.children.length" v-model="formEl.children" group="FormItem" chosenClass="chosen" forceFallback="true" animation="500" :component-data="getColSpan(formEl.children)">
                  <div class="emptyContainerText">emptyContainerText</div>
                </draggable>

              </Row>

              <RenderCmp v-else :item="formEl" :main-cmp="mainCmp" :meta-data="metaData" :is-wysiwyg="true" />
            </span>

            <!-- EMPTY DUMMY FOR INCOMING Drop -->
            <div v-if="!formItem.children || !formItem.children.length" class="emptyContainerText">emptyContainerText</div>
          </draggable>
        </FormItem>
      </draggable>
    </Form>
  </div>

此路不通,于是考虑能不能读取 JSON 元数据,然后让 Vue 渲染即可(如果要额外修改一些组件的如 DD,在解析过程中配置即可)。这个很容易联想到 Render 函数去解决。解析递归遍历 JSON 都不是问题,比较头大的是 createElement() 函数(或叫 h()),每个节点都要对其 h() 返回 VNode,返回的结构给父节点作为参数传入。这个我尝试了很多方法都不行,唉 还是数据结构的知识不行。

没办法,只能求救网络,搜索一番找到两个方法:

第一个方法依然还是使用递归,就几行代码给完美地解决了……

/**
* Create component is a recursive function that takes a DOM structure
* and a rendering function (of vue.js) and returns a Vuejs component.
*/
const createComponent = (dNode, h: CreateElement) => 
    // Handle empty elements and return empty array in case the dNode passed in is empty
    if (_.isEmpty(dNode))
        return [];

    // if the el is array call createComponent for all nodes
    if (_.isArray(dNode))
        return dNode.map((child) => createComponent(child, h));

    let children = [];

    if (dNode.children && dNode.children.length > 0) 
        dNode.children.forEach((c) => 
            if (_.isString(c)) 
                children.push(c)
             else 
                children.push(createComponent(c, h))
            
        );
    
    // Need to clone 
    const properties = _.cloneDeep(dNode.properties)
    return h(dNode.tagName, properties, children.length > 0 ? children : dNode.textNode)

非标签方法使用 Vue.Draggable

既然使用了 Render 生成,那么 Vue.Draggable 也不能用标签。但全网几乎没有人那么做,还只能找到一个老外的:How to make Vue-Draggable work with different structure of elements/components,但他用的有点奇怪。

其实是可行的,问题在于没有 v-model……

绑定数据

开明宗义,Vue 中 Render 函数里没有与 v-model 相对应的 API,因此若想实现双向的数据绑定需要自己来实现逻辑。

一开始标签的时候,Vue.Draggable 帮我做了很多事,我不禁 Vue.Draggable 的强大,秒实现拖放并同步修改数据。但改成 Render 后,我的心态是这样的。

没办法,自己写。但后来觉得也不是那么难……

 /**
  * 配置可以拖放
  * 
  * @param children  数组
  * @param group     分组
  * @returns 组件的配置,特别这是给 <draggable> 元素用的属性
  */
 makeDragDropProp(children: RenderedMeta[], group: string): any 
     return 
         props:  list: children ,
         attrs:  group: group, chosenClass: "chosen", forceFallback: true, animation: 500 ,
         on: 
             /**
              * 使用 Render/Vnode 的代价是无法使用 V-Model 双向绑定。
              * 此时 Vue.Draggable 令 DOM 变化了但没同步修改 Data,要自己给出。
              * 百度不了任何资料。不过自己写,其实也不难。
              * 主要是 list 输入数据(Array),然后 change 事件里面同步数据
              * 
              * @param $event 
              */
             change: ($event: any): void => 
                 let e: any;

                 if ($event.moved) 
                     e = $event.moved;
                     let newIndex: number = e.newIndex, oldIndex: number = e.oldIndex;
                     let old: any = children[oldIndex];
                     let temp: any = children[newIndex];
                     children[newIndex] = old;
                     children[oldIndex] = temp;
                  else if ($event.added) 
                     e = $event.added;
                     children.splice(e.newIndex, 0, e.element);
                  else if ($event.removed) 
                     e = $event.removed;
                     children.splice(e.oldIndex, 1);
                  else 
                     alert('todo');
                     debugger
                 

                 this.$forceUpdate();
             ,
             end: () => 
                 this.ActiveWdiget = null;
             
         
     
 ,

tag 属性的使用

在 iView 组件中,Row DOM 下面不能有因为 DD 生成的 div,那样会破坏 Row/Col 的结构,故用 tag,减少一层 div。于是这时候 tag 属性可以派上用场了。

参考

以上是关于Vue.Draggable 心得的主要内容,如果未能解决你的问题,请参考以下文章

Vue.Draggable 心得

解决 vue.draggable 拖拽 点击事件失效无效与拖拽事件冲突的问题

Vue.Draggable

VueJS + Vue.Draggable + Vuex Store + 计算变量

Vue3与Vue.draggable-next拖拽问题

Vue.Draggable学习总结