代码之家  ›  专栏  ›  技术社区  ›  Sphinx

vue自定义指令使用更新的dom(或$el)

  •  1
  • Sphinx  · 技术社区  · 8 年前

    我要设计一个自定义指令,将“cx”替换为 <strong>cx</strong> 对于dom树中的所有textnode。

    以下是迄今为止我所做的尝试:

    Vue.config.productionTip = false
    
    function removeKeywords(el, keyword){
      if(!keyword) return
      let n = null
      let founds = []
      walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
      while(n=walk.nextNode()) {
        if(n.textContent.trim().length < 1) continue
        founds.push(n)
      }
      let result = []
      founds.forEach((item) => {
        if( new RegExp('cx', 'ig').test(item.textContent) ) {
          let kNode = document.createElement('span')
          kNode.innerHTML = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
          item.parentNode.insertBefore(kNode, item)
          item.parentNode.removeChild(item)
        }
      })
    }
    
    let myDirective = {}
    myDirective.install = function install(Vue) {
      let timeoutIDs = {}
      Vue.directive('keyword-highlight', {
        bind: function bind(el, binding, vnode) {
          clearTimeout(timeoutIDs[binding.value.id])
          if(!binding.value) return
          timeoutIDs[binding.value.id] = setTimeout(() => {
            removeKeywords(el, binding.value.keyword)
          }, 500)
        },
        componentUpdated: function componentUpdated(el, binding, vnode) {
          clearTimeout(timeoutIDs[binding.value.id])
          timeoutIDs[binding.value.id] = setTimeout(() => {
            removeKeywords(el, binding.value.keyword)
          }, 500)
        }
      });
    };
    Vue.use(myDirective)
    app = new Vue({
      el: "#app",
      data: {
        keyword: 'abc',
        keyword1: 'xyz'
      },
      methods: {
      }
    })
    .header {
      background-color:red;
    }
    
    strong {
      background-color:yellow
    }
    <script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
    <div id="app">
      <input v-model="keyword">
      <input v-model="keyword1">
      <h1>Test Case 1: try to change 2nd input to <span class="header">anything</span></h1>
      <div v-keyword-highlight="{keyword:keyword, id:1}">
        <p>Test1<span>Test2</span>Test3<span>{{keyword}}{{keyword1}}</span></p>
      </div>
      <h1>Test Case 2 which is working</h1>
      <div :key="keyword+keyword1" v-keyword-highlight="{keyword:keyword, id:2}">
        <p>Test1<span>Test2</span>Test3<span>{{keyword}}{{keyword1}}</span></p>
      </div>
    </div>

    第一个案例 :应该是相关Vnode已被替换为 <span><strong></strong></span> ,因此将无法使用数据属性正确更新。

    第二种情况 :按预期工作。添加解决方案 :key 为了强制装入组件,所以当触发更新时,它将使用模板和最新的数据属性进行渲染,然后装入。

    但是我更喜欢在指令钩子中强制挂载而不是绑定 关键 或者根据模板和最新的数据属性获取更新的dom($el)。所以其他任何想使用这个指令的人都不需要 关键

    非常感谢。

    2 回复  |  直到 8 年前
        1
  •  2
  •   Richard Matsen    8 年前

    我不确定这是否是最好的做法,因为有人警告不要修改 vnode ,但这在示例中可以动态添加密钥

    vnode.key = vnode.elm.innerText
    

    奇怪的是我注意到 第一 componentUpdated 但是第二个没有,即使第二个内部元素更新了它们的值,但是第一个没有更新,这与您预期的相反。

    请注意,发生更改的原因是第二个实例调用 bind 当输入发生变化时,不是因为中的代码 组件已更新 .

    console.clear()
    Vue.config.productionTip = false
    
    function removeKeywords(el, keyword){
      console.log(el, keyword)
      if(!keyword) return
      let n = null
      let founds = []
      walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
      while(n=walk.nextNode()) {
        if(n.textContent.trim().length < 1) continue
        founds.push(n)
      }
      let result = []
      founds.forEach((item) => {
        if( new RegExp('cx', 'ig').test(item.textContent) ) {
          let kNode = document.createElement('span')
          kNode.innerHTML = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
          item.parentNode.insertBefore(kNode, item)
          item.parentNode.removeChild(item)
        }
      })
    }
    
    let myDirective = {}
    myDirective.install = function install(Vue) {
      let timeoutIDs = {}
      Vue.directive('keyword-highlight', {
        bind: function bind(el, binding, vnode) {
          console.log('bind', binding.value.id)
          clearTimeout(timeoutIDs[binding.value.id])
          if(!binding.value) return
          vnode.key = vnode.elm.innerText
          timeoutIDs[binding.value.id] = setTimeout(() => {
            removeKeywords(el, binding.value.keyword)
          }, 500)
        },
        componentUpdated: function componentUpdated(el, binding, vnode) {
          //clearTimeout(timeoutIDs[binding.value.id])
          //timeoutIDs[binding.value.id] = setTimeout(() => {
            //removeKeywords(el, binding.value.keyword)
          //}, 500)
        }
      });
    };
    Vue.use(myDirective)
    app = new Vue({
      el: "#app",
      data: {
        keyword: 'abc',
        keyword1: 'xyz'
      },
      methods: {
      }
    })
    .header {
      background-color:red;
    }
    
    strong {
      background-color:yellow
    }
    <script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
    <div id="app">
      <input v-model="keyword">
      <input v-model="keyword1">
      <h1>Test Case 1: try to change 2nd input to <span class="header">anything</span></h1>
      <div v-keyword-highlight="{keyword:keyword, id:1}">
        <p>Test1<span>Test2</span>Test3<span>{{keyword}}{{keyword1}}</span></p>
      </div>
      <h1>Test Case 2 which is working</h1>
      <div :key="keyword+keyword1" v-keyword-highlight.keyword1="{keyword:keyword, id:2}">
        <p>Test1<span>Test2</span>Test3<span>{{keyword}}{{keyword1}}</span></p>
      </div>
    </div>
        2
  •  1
  •   Sphinx    8 年前

    我发现Vue的用途 Vue. patch 比较旧/新节点,然后生成dom元素。

    检查 Vue Github Lifecycle source code ,因此第一个元素可以是一个将被装载的DOM对象。

    所以我按照步骤使用 the directive hooks (bind、componentupdated、update等)生成新的dom元素,然后将其复制到指令挂钩的第一个参数。

    最后,下面的演示似乎有效:没有强制重新装载,只重新编译Vnode。

    聚苯乙烯 :我使用DeepClone方法克隆 vnode 因为函数内部 __patch__(oldNode, newNode, hydrating) ,它将修改 newNode

    聚苯乙烯 作为 Vue directive access its instance 说,在指令的钩子里,使用 vnode.context 访问实例。

    编辑 :在下面循环所有儿童 test ,然后附加到 el ,简单副本 test.innerHTML el.innerHTML 会导致一些问题,如按钮不工作。

    然后在我的实际项目中测试这个指令 <div v-keyword-highlight>very complicated template</div> 到目前为止一切正常。

    function deepClone (vnodes, createElement) {
      let clonedProperties = ['text', 'isComment', 'componentOptions', 'elm', 'context', 'ns', 'isStatic', 'key']
      function cloneVNode (vnode) {
        let clonedChildren = vnode.children && vnode.children.map(cloneVNode)
        let cloned = createElement(vnode.tag, vnode.data, clonedChildren)
        clonedProperties.forEach(function (item) {
          cloned[item] = vnode[item]
        })
        return cloned
      }
      return vnodes.map(cloneVNode)
    }
    
    function addStylesForKeywords(el, keyword){
      if(!keyword) return
      let n = null
      let founds = []
      walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
      while(n=walk.nextNode()) {
        if(n.textContent.trim().length < 1) continue
        founds.push(n)
      }
      let result = []
      founds.forEach((item) => {
        if( new RegExp('cx', 'ig').test(item.textContent) ) {
          let kNode = document.createElement('span')
          kNode.innerHTML = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
          item.parentNode.insertBefore(kNode, item)
          item.parentNode.removeChild(item)
        }
      })
    }
    
    let myDirective = {}
    myDirective.install = function install(Vue) {
      let timeoutIDs = {}
      let temp = Vue.extend({
      template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
      })
      let fakeVue = new temp()
      Vue.directive('keyword-highlight', {
        bind: function bind(el, binding, vnode) {
          clearTimeout(timeoutIDs[binding.value.id])
          if(!binding.value) return
          timeoutIDs[binding.value.id] = setTimeout(() => {
            addStylesForKeywords(el, binding.value.keyword)
          }, 500)
        },
        componentUpdated: function componentUpdated(el, binding, vnode) {
          let fakeELement = document.createElement('div')
          //vnode is readonly, but method=__patch__(orgNode, newNode) will load new dom into the second parameter=newNode.$el, so uses the cloned one instead
          let clonedNewNode = deepClone([vnode], vnode.context.$createElement)[0]
          let test = clonedNewNode.context.__patch__(fakeELement, clonedNewNode)
    
          while (el.firstChild) {
              el.removeChild(el.firstChild);
          }
          test.childNodes.forEach((item) => {
            el.appendChild(item)
          })
          clearTimeout(timeoutIDs[binding.value.id])
          timeoutIDs[binding.value.id] = setTimeout(() => {
            addStylesForKeywords(el, binding.value.keyword)
          }, 500)
        }
      });
    };
    Vue.use(myDirective)
    Vue.config.productionTip = false
    app = new Vue({
      el: "#app",
      data: {
        keyword: 'abc',
        keyword1: 'xyz'
      },
      methods: {
        changeData: function () {
          this.keyword += 'c'
          this.keyword1 = 'x' + this.keyword1
          console.log('test')
        }
      }
    })
    .header {
      background-color:red;
    }
    
    strong {
      background-color:yellow
    }
    <script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
    <script src="https://unpkg.com/lodash"></script>
    <div id="app">
      <input v-model="keyword">
      <input v-model="keyword1">
      <h4>Test Case 3 <span class="header"></span></h4>
      <div v-keyword-highlight="{keyword:keyword, id:1}">
        <p>Test1<span>Test2</span>Test3<span>{{keyword}}{{keyword1}}</span></p>
        <button @click="changeData()">Click me</button>
      </div>
    </div>