Vue custom directive uses the updated Dom (or $el) - javascript

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.

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>

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>

Related

VUE Count how many directives are used on a component

I've tried to count how many directives are used on a component like this. But it does not work as I expected
this is my directive file
import ahoy from "ahoy.js"
let count = 0
export default {
id: "bar",
definition: {
bind: (el, binding) => {
const handler = (entries, observer) => {
count++
console.log(count)
if (entries[0].isIntersecting) {
setTimeout(() => {
ahoy.track("impression", {
...(typeof binding.value === "object"
? { ...binding.value }
: { value: binding.value }),
page: document.title,
path: window.location.pathname.replace(/^\/en\//g, "/"),
class: el.classList.value
})
observer.unobserve(entries[0].target)
}, 100)
}
}
const createIntersection = new IntersectionObserver(handler, { rootMargin: "-45% 0%" })
createIntersection.observe(el)
}
}
}
and this is how I call directive on my component
ReviewCard(
v-bar="createIntersection(foo)"
)
variable count not stored val++
how can I count how many directives are used on a component?
Thanks in advance :)
count++ is currently in handler, which is passed to the IntersectionObserver, so count would only be incremented upon an intersection. That update should probably be moved outside of handler to the root of the bind() call:
export default {
definition: {
bind: (el, binding) => {
count++
const handler = /*...*/
//...
}
}
}

Global loaded data in VueJs is occasionally null

I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.

Call a class method from a another class in JavaScript?

I am doing a task where I need to wire up a search field to a simple JS application that displays a few items and the user can search through and filter them.
There are three classes - App, ProductsPanel and Search. Both Search and ProductsPanel are being initialised inside the App class.
The ProductsPanel class holds an array with 10 products.
I want to call a method of ProductsPanel from inside Search that filters through the products. How can I do that?
I've tried using this.productsPanel = new productsPanel() inside the constructor of the first class, but that brings up a new instance which doesn't have the array of all of the products.
Here's the App class:
class App {
constructor() {
this.modules = {
search: {
type: Search,
instance: null
},
filter: {
type: Filter,
instance: null
},
productsPanel: {
type: ProductsPanel,
instance: null
},
shoppingCart: {
type: ShoppingCart,
instance: null
}
};
}
init() {
const placeholders = document.querySelectorAll("#root [data-module]");
for (let i = 0; i < placeholders.length; i++) {
const root = placeholders[i];
const id = root.dataset.module;
const module = this.modules[id];
if (module.instance) {
throw new Error(`module ${id} has already been started`);
}
module.instance = new module.type(root);
module.instance.init();
// console.info(`${id} is running...`);
}
}
}
app = new App();
app.init();
And here are the Search:
export default class Search {
constructor(root) {
this.input = root.querySelector("#search-input");
}
// addEventListener is an anonymous function that encapsulates code that sends paramaters to handleSearch() which actually handles the event
init() {
this.input.addEventListener("input", () => {
this.handleSearch();
});
}
handleSearch() {
const query = this.input.value;
app.modules.productsPanel.instance.performSearch(query);
}
}
And ProductsPanel classes:
export default class ProductsPanel {
constructor(root) {
this.view = new ProductsPanelView(root, this);
this.products = [];
}
init() {
this.products = new ProductsService().products;
this.products.forEach(x => this.view.addProduct(x));
}
performSearch(query) {
query = query.toLowerCase();
this.products.forEach(p => {
if (query === p.name) {
this.view.showProduct(p.id);
} else {
this.view.hideProduct(p.id);
}
});
}
addToCart(id) {
const product = this.products.filter(p => p.id === id)[0];
if (product) {
app.modules.shoppingCart.instance.addProduct(product);
}
}
}
I want to call ProductsPanel's performSearch method but on the instance created by the App class. I have no clue on how I can do that.
Try below custom event handler class
class CustomEventEmitter {
constructor() {
this.eventsObj = {};
}
emit(eName, data) {
const event = this.eventsObj[eName];
if( event ) {
event.forEach(fn => {
fn.call(null, data);
});
}
}
subscribe(eName, fn) {
if(!this.eventsObj[eName]) {
this.eventsObj[eName] = [];
}
this.eventsObj[eName].push(fn);
return () => {
this.eventsObj[eName] = this.events[eName].filter(eventFn => fn !== eventFn);
}
}
}
How to use?
create the object of CustomEventEmitter class
let eventEmitter = new CustomEventEmitter()
Subscribe an event
emitter.subscribe('event: do-action', data => {
console.log(data.message);
});
call the event
emitter.emit('event: do-action',{message: 'My Custom Event handling'});
Hope this helps!

unable to select all checkboxes in tree using angular2-tree on init

Objective : i have a button named "feed data" so when ever i click it the data will be loaded i mean the tree with checkboxes here my requirement is when ever i click it along with data all the check boxes have to be checked on init i tried using
this.treeComp.treeModel.doForAll((node: TreeNode) => node.setIsSelected(true));
but it is not working below is my code
click(tree: TreeModel) {
this.arrayData = [];
let result: any = {};
let rs = [];
console.log(tree.selectedLeafNodeIds);
Object.keys(tree.selectedLeafNodeIds).forEach(x => {
let node: TreeNode = tree.getNodeById(x);
// console.log(node);
if (node.isSelected) {
if (node.parent.data.name) //if the node has parent
{
rs.push(node.parent.data.name + '.' + node.data.name);
if (!result[node.parent.data.name]) //If the parent is not in the object
result[node.parent.data.name] = {} //create
result[node.parent.data.name][node.data.name] = true;
}
else {
if (!result[node.data.name]) //If the node is not in the object
result[node.data.name] = {} //create
rs.push(node.data.name);
}
}
})
this.arrayData = rs;
tree.selectedLeafNodeIds = {};
}
selectAllNodes() {
this.treeComp.treeModel.doForAll((node: TreeNode) => node.setIsSelected(true));
// firstNode.setIsSelected(true);
}
onTreeLoad(){
console.log('tree');
}
feedData() {
const results = Object.keys(this.data.info).map(k => ({
name: k,
children: this.data.info[k].properties
? Object.keys(this.data.info[k].properties).map(kk => ({ name: kk }))
: []
}));
this.nodes = results;
}
feedAnother() {
const results = Object.keys(this.dataa.info).map(k => ({
name: k,
children: this.dataa.info[k].properties
? Object.keys(this.dataa.info[k].properties).map(kk => ({ name: kk }))
: []
}));
this.nodes = results;
}
onActivate(event) {
this.selectedDataList.push(event.node.data);
console.log(this.selectedDataList)
}
onDeactivate(event) {
const index = this.selectedDataList.indexOf(event.node.data);
this.selectedDataList.splice(index, 1);
console.log(this.selectedDataList)
}
below is my stackblitz https://stackblitz.com/edit/angular-hrbppy
Use updatedata and initialized event to update the tree view to check all checkboxes.
app.component.html
<tree-root #tree *ngIf ="nodes" [nodes]="nodes" [options]="options" [focused]="true"
(initialized)="onTreeLoad()"
(updateData)="updateData()"
(select)="onActivate($event)"
(deselect)="onDeactivate($event)">
</tree-root>
It'll initiate tree-root component only if nodes variable is available,
then in the initialized and updateData event call selectAllNodes method to select all checkboxes.
app.component.ts
updateData() {
this.selectAllNodes();
}
onTreeLoad(){
this.selectAllNodes();
}
Refer to this slackblitz for working example.
just, in your function feed data call to your function this.selectAllNodes() enclosed in a setTimeout. You can see your forked stackblitz
setTimeout(()=>{
this.selectAllNodes()
})
NOTE: I see in your code you try to control in diferents ways the items selected. I simplified using a recursive function.
In this.treeComp.treeModel.selectedLeafNodeIds we have the items that are changed, so
getAllChecked()
{
const itemsChecked=this.getData(
this.treeComp.treeModel.selectedLeafNodeIds,null)
console.log(itemsChecked);
}
getData(nodesChanged,nodes) {
nodes=nodes||this.treeComp.treeModel.nodes
let data: any[] = []
nodes.forEach((node: any) => {
//in nodesChanged we has object like {1200002:true,123132321:false...}
if (nodesChanged[node.id]) //can be not changed, and then it's null because
//it's not in object or can be changed to false
data.push({id:node.id,name:node.name})
//or data.push(node.name); //if only need the "name"
if (node.children)
data=[...data,...this.getData(nodesChanged,node.children)]
}
);
return data
}
Updated I updated the function getData to include the "parent" of the node, but looking the code of #Raghul selvam, his function like me more than mine.
getData(nodesChanged,nodes,prefix) {
nodes=nodes||this.treeComp.treeModel.nodes
let data: any[] = []
nodes.forEach((node: any) => {
if (nodesChanged[node.id])
data.push(prefix?prefix+"."+node.name:node.name)
if (node.children)
data=[...data,...this.getData(nodesChanged,node.children,prefix?prefix+"."+node.name:node.name)]
}
);
return data
}
And call it as
this.getData(this.treeComp.treeModel.selectedLeafNodeIds,null,"")
You could add this in your onTreeLoad function. You could add a boolean flag(treeLoaded) for tracking if the tree has loaded or not.
onTreeLoad(tree){
this.selectAllNodes();
this.treeLoaded = true;
}

Variable is pushed, but it doesn't exist later

I have the following code:
const scenarioList = []
const randomScenario = () => {
return scenarioList[Math.floor(Math.random() * scenarioList.length--)]
}
class Scenario{
setBG(){
//screen.bg = this.bg
//screen.redraw()
}
write(text, buttons, callback){
//$('#gametext > span').html(`<span>${text}</span>`)
//input.setText(buttons)
//input.bindAll(callback)
}
constructor(imgsrc, text, actions, callback){
let img = new Image()
img.src = imgsrc
this.bg = img
this.text = text
this.actions = actions
this.callback = callback
scenarioList.push(this)
console.log(scenarioList)
}
}
I init the class the following (and this is in the global scope)
new Scenario('./bg/1.png', 'You look around and see a huge mountain, what do you do?',[
'Climb It!!',
'Walk around',
'Other Direction',
'Rest',
], [
() => {
alert('a')
},
() => {
alert('a')
},
() => {
alert('a')
},
() => {
alert('a')
},
])
And verify with console.log(scenarioList)
[Scenario]
So its appended, but when I later try to do a console.log() on the same variable it is the following:
[]
Code that causes it:
const startGame = () => {
alert('were here') // this executes at the correct time, but later then variable init.
let scn = randomScenario()
console.log(scenarioList)
scn.write()
scn.setBG()
}
I am not seeing why this would happen, anyone can give me a push in the right direction?
I've found the solution, this code actually removed the element from the array:
const randomScenario = () => {
return scenarioList[Math.floor(Math.random() * scenarioList.length--)]
}
instead I did this:
return scenarioList[Math.floor(Math.random() * scenarioList.length -1)]

Categories