I have a vue app containing a vue-multiselect and I want to load the multiselect options through ajax. I am using lodash.throttle to throttle the firing of ajax requests as the user types in the search criteria. But whatever I do, I am seeing multiple requests being fired for each character I type in search. What I am doing wrong? Thanks in advance.
<template>
<multiselect :options="allLocations.map(p => p.externalId)"
:searchable="true"
:custom-label="uuid => {const sel = allLocations.filter(s => s.externalId === uuid); return sel.length === 1 ? sel[0].name + ' (' + sel[0].type + ')' : '';}"
class="mx-1 my-1"
style="width:500px;"
v-model="locations"
:clear-on-select="true"
:close-on-select="false"
:show-labels="true"
placeholder="Pick Locations to filter"
:multiple="true"
:loading="locationsLoading"
:internal-search="false"
#search-change="findLocations"
#input="updateLocations"
:allowEmpty="true" />
</template>
<script>
import {throttle} from 'lodash'
export default {
name: 'test-throttle-component',
data() {
allLocations: [],
locationsLoading: false,
locations: [],
},
methods: {
findLocations(search) {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
this.locationsLoading = true
const self = this
throttle(() => self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}), 5000)()
},
updateLocations() {
const self = this
this.$store.dispatch('updateSelectedLocations', this.locations)
.then(() => self.emitRefresh())
},
}
}
</script>
#strelok2010 is nearly right but I think he overlooked the fact that the vue multiselect #search-change handler expects a handler that takes the search argument and so the code won't work as is. Also I think, this won't resolve to the component inside the arrow function and so you may have to use standard JS function. Here is what I think would work.
findLocations: throttle(function(search) {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
const self = this
this.locationsLoading = true
self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}
}, 5000)
Try to wrap findLocations method to throttle function:
findLocations: throttle(() => {
this.$log.debug("Going to find locations for search criteria", search)
const params = {search: search}
const self = this
this.locationsLoading = true
self.$http.get("locations/ddlist", {params}).then(res => {
self.allLocations = res.data.items
self.locationsLoading = false
}
}, 5000)
More info here
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>
I have a loading screen that takes about 15-30 seconds depending on the data that's loaded. It loads about 50 items and displays on the page:
Loading item x
It uses an observable/subscription for each data call made to the DB. Upon receiving the data, the subscription fires off and adds it to an HTML string:
sync() {
this.syncStatus = "Starting sync"
this.syncService.sync().subscribe((status: string) => {
this.syncStatus += "<div>" + status + '</div>';
}, (error: string) => {
console.log(error);
}, () => {
this.Redirect();
});
}
<div class="description">
<span [innerHTML]="syncStatus"></span>
</div>
As of now, it simply shows the list and it cuts off the list display because it gets so long (again, 50+ items, sometimes hundreds). I was wondering, how do I show each individual item to the page for 5s then hide it?
You can create an array of objects with the insertion time of the item, then filter the array based on this property.
sync() {
this.syncStatus = [{ msg: 'Starting Sync', time: Date.now() }];
this.syncService.sync().subscribe((status: string) => {
this.syncStatus.unshift(status);
this.removeOldEntries();
}, (error: string) => {
console.log(error);
}, () => {
this.Redirect();
});
}
Then filter old entries:
removeOldEntries() {
this.syncStatus = this.syncStatus.filter((status) => status.time < Date.now() - 300000); // 5 minutes
}
It would be great if you take advantage of components in Angular
Stack Blitz, Source Code
Explanation
You don't need date creation to check when a data was received
You don't need filtering through hordes of data
If you use Angular component approach, life would be easier, Each component will be responsible for removing itself
Main Component.ts
export class AppComponent {
data = [
"Hello 0"
];
count = 1;
ngOnInit() {
// Think of this as your subscription to backend
setInterval(() => {
if (!this.data) {
this.data = []
}
this.data.push("Hello " + this.count ++);
}, 1000);
}
}
Main Component.html
<div class="description">
<div *ngFor="let datum of data; let i = index">
<hello [ref]="data" [index]="i">{{datum}}</hello>
</div>
</div>
Hello.ts
#Component({
selector: 'hello',
template: `<ng-content></ng-content>`
})
export class HelloComponent {
#Input() ref;
#Input() index: number;
ngOnInit() {
// This code will remove this component, upon
// the timeout you specify
setTimeout(() => {
this.ref.splice(this.index, 1);
}, 5000);
}
}
I have a code that gets json from RESTful API. but It only shows .container and It says that there's nothing in items array. the mysterious thing is it doesn't show any errors about it. so I was trying to debug it showing result from fetch using console.log, so I added like let result = await fetch('video').then(res => res.json()) under the code but It doesn't show anything on browser console. seems like It doesn't run the async getData function but I have no clue..
<template lang="pug">
.container
.columns(v-for="n in lines")
.column.is-3.vid(v-for='item in items')
.panel
p.is-marginless
a(:href='item.videoId')
img(:src='item.thumbnail')
.panel.vidInfo
.columns.hax-text-centered
.column
.panel-item.reddit-ups
span {{ item.score }}
i.fa.fa-reddit-alien.fa-2x
.panel-item.reddit-date
i.fa.fa-calendar.fa-2x
</template>
<script>
export default {
name: 'main',
data: () => ({
items: [],
lines: 0
}),
async getVideo () {
this.items = await fetch('/video').then(res => res.json())
this.lines = Math.ceil(this.items.length/4)
}
}
</script>
There are few issues in your code, and console should warn you about them.
First define data object as ES6 Object Method Shorthand, try to avoid arrow functions:
data() {
return {
items: [],
lines: 0
}
}
Then I guess get video is method, so It should be placed under the methods object:
methods: {
async getVideo () {
this.items = await fetch('/video').then(res => res.json())
this.lines = Math.ceil(this.items.length/4)
}
}
I don't know where you want trigger this method (on click, when instance is created or mounted), but I will use created hook
<script>
export default {
name: 'main',
data() {
return {
items: [],
lines: 0
}
},
methods: {
// I don't think you need async/await here
// fetch would first return something called blob, later you can resolve it and get your data
// but I suggest you to use something like axios or Vue reource
async getVideo () {
await fetch('/video')
.then(res => res.json())
.then(items => this.items = items)
this.lines = Math.ceil(this.items.length/4)
}
},
created() {
this.getVideo()
}
}
</script>
I am running a function to get a list of categories and for each category, get an id and run another function to get the number of 'links' in that particular category I have got the id.
For that, I have the following code:
ngOnInit(): void
{
this.categories = this.categoryService.getCategories();
const example = this.categories.mergeMap((categor) => categor.map((myCateg) =>
{
this.categoryService.countCategoryLinks(myCateg.id)
.map(numlinks => Object.assign(myCateg,{numLinks: numlinks}))
.subscribe(valeur => console.log(valeur));
return myCateg.id
}));
example.subscribe(val => console.log("valeur2: "+val));
}
where getCategories() is:
getCategories(): Observable<any>
{
return this.category.find({where: {clientId: this.userApi.getCurrentId()}})
};
and countCategoryLinks() is:
countCategoryLinks(id: number): Observable<any>
{
return this.category.countLinks(id)
};
It appears, as shown in the screenshot below:
that numlinks is an Object. Of course this is done by Object.assign.
Is there a way to have the "count" inserted like categoryName ot clientId instead of an Object?
My goal is to be able to show all the values in a template:
<tr *ngFor="let category of categories | async; let id = index">
<td>
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect mdl-data-table__select"
for="row[3]">
<input type="checkbox" id="row[3]" class="mdl-checkbox__input"/>
</label>
</td>
<td class="mdl-data-table__cell--non-numeric"><a [routerLink]="['/category/links']"></a>{{
category.categoryName }}
</td>
<td class="mdl-data-table__cell--non-numeric">{{category.numLinks}}</td>
<td #category.id><i (click)="deleteCategory(id)" class="material-icons">delete</i></td>
</tr>
If I understand this correctly, then you can simply do this:
.map(numlinks => {
myCateg.numLinks = numlinks.count;
return myCateg;
})
Instead of
.map(numlinks => Object.assign(myCateg,{numLinks: numlinks}))
Therefore your code should become something like this:
{
this.categories = this.categoryService.getCategories();
const example = this.categories
.mergeMap((categor) => categor
.map((myCateg) => {
this.categoryService
.countCategoryLinks(myCateg.id)
.map(numlinks => {
myCateg.numLinks = numlinks.count;
return myCateg;
})
.subscribe(valeur => console.log(valeur));
return myCateg.id;
}));
example.subscribe(val => console.log("valeur2: " + val));
}
Change this line:
.map(numlinks => Object.assign(myCateg,{numLinks: numlinks}))
to:
.map(numlinks => Object.assign(myCateg, { numLinks: numlinks.count }))