问题
I want to design one custom directive to replace 'cx' to <strong>cx</strong>
for all TextNodes in the Dom Tree.
Below is what I had tried so far:
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>
First Case: It should be caused by related VNode already been replaced by <span><strong></strong></span>
, so will not get updated with the data properties correctly.
Second Case: It works as expected. The solution is added :key
to force mount the component, so when update is triggered, it will render with the template and latest data properties then mount.
But I prefer to force mount in the directive hook instead of bind :key
at the component, or get the updated Dom($el) based on the template and the latest data properties. so anyone else who want to use this directive doesn't need to case about the :key
.
Many thanks for any.
回答1:
I'm not sure this is the best practice since there are warnings against modifying vnode
, but this works in your sample to dynamically add the key
vnode.key = vnode.elm.innerText
The weird thing I notice that the first directive responds to componentUpdated
but the second does not, even though the second inner elements update their values but the first does not - which is contrary to what you would expect.
Note that the change occurs because the second instance calls bind
again when the inputs change, not because of the code in componentUpdated
.
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:
I found Vue uses Vue.patch to compare old/new nodes then generate out Dom elements.
Check Vue Github Lifecycle source code, so the first element can be one Dom object which will be mounted.
So I follow the steps to uses the third parameter of the directive hooks (bind, componentUpdated, update etc) to generate new Dom elements, then copy it to the first parameter of the directive hooks.
Finally below demo seems work: no force re-mount, only re-compile VNodes.
PS: I uses deepClone methods to clone vnode
because inside of the function __patch__(oldNode, newNode, hydrating)
, it will modify newNode
.
PS: As Vue directive access its instance said, inside the hooks of the directive, uses vnode.context
to access the instance.
Edit: loop all childrens under test
, then append to el
, simple copy test.innerHTML
to el.innerHTML
will cause some issues like the button is not working.
Then test this directive in my actual project like <div v-keyword-highlight>very complicated template</div>
, it is working fine so far.
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>
来源:https://stackoverflow.com/questions/50594818/vue-custom-directive-uses-the-updated-dom-or-el