I'm working in a Vue component that I'll publish when it's finished that wraps Clusterize.js (there is a vue-clusterize component but it only works for v1.x). What I want to achieve is to render a huge list of items pretty fast using Vue. I actually need it for a table. I tried with vue-virtual-scroll but it doesn't support tables and the performance is not that good. So I wanted to try with Clusterize.js.
Because I want this component to be highly configurable I decided that you will be able to provide a scoped slot for each row of the items list where you will receive the item. The problem is when I try to assign the scoped slot from the clusterize componets to each row before mounting the component it doesn't work.
Here you have some snippets of my code (it is just a mvp)
clusterize.vue
Template
<div class="clusterize">
<table>
<thead>
<tr>
<th>Headers</th>
</tr>
</thead>
</table>
<div
ref="scroll"
class="clusterize-scroll">
<table>
<tbody
ref="content"
class="clusterize-content">
<tr class="clusterize-no-data">
<td>Loading...</td>
</tr>
</tbody>
</table>
</div>
Script
import Vue from 'vue';
import Clusterize from 'clusterize.js';
export default {
name: 'Clusterize',
props: {
items: {
type: Array,
required: true,
},
},
data() {
return {
clusterize: null,
};
},
computed: {
rows() {
return this.items.map(item => '<tr><slot :item="1"/></tr>');
},
},
watch: {
rows() {
this.clusterize.update(this.rows);
},
},
mounted() {
const scrollElem = this.$refs.scroll;
const contentElem = this.$refs.content;
this.clusterize = new Clusterize({
rows: this.rows,
scrollElem,
contentElem,
});
this.clusterize.html = (template) => {
contentElem.innerHTML = template;
const instance = new Vue({ el: contentElem });
instance.$slots = this.$slots;
instance.$scopedSlots = this.$scopedSlots;
instance.$mount();
console.log(instance.$scopedSlots); // empty
console.log(instance.$slots) // not empty
};
},
};
component.vue
<clusterize :items="test">
<template slot-scope="props">
item
</template>
</clusterize>
The thing is that if it don't use a scoped slot it works perfectly but I really need to use them otherwise the component doesn't have any sense.
I'll appreciate any help or advice.
Thank you so much in advance.
The issue should be caused by mount different Vue instance to same el multiple times (please look into the second demo, you shouldn't mount multiple instances to same element, the following instances will not mount since the element is already “blocked” by first instance).
My solution: create Vue instance (doesn't bind to el) in the air then take vm.$el as the output.
Please look into below simple demo,
Vue.config.productionTip = false
Vue.component('clusterize', {
template: `<div class="clusterize">
<table>
<thead>
<tr>
<th>Headers</th>
</tr>
</thead>
</table>
<div
ref="scroll"
class="clusterize-scroll">
<table>
<tbody
ref="content"
id="clusterize-id"
class="clusterize-content">
<tr class="clusterize-no-data">
<td>Loading...</td>
</tr>
</tbody>
</table>
</div></div>`,
props: {
items: {
type: Array,
required: true,
},
},
data() {
return {
clusterize: null,
clusterVueInstance: null
};
},
computed: {
rows() {
return this.items.map(item => {
return '<tr><td><span>' +item+'</span><slot :item="1"/></td></tr>'
});
},
},
watch: {
rows() {
this.clusterize.update(this.rows);
},
},
mounted() {
const scrollElem = this.$refs.scroll;
const contentElem = this.$refs.content;
this.clusterize = new Clusterize({
rows: this.rows,
scrollElem,
contentElem,
});
this.clusterize.html = (template) => {
this.clusterize.content_elem.innerHTML = template;
if(this.clusterVueInstance) {
this.clusterVueInstance.$destroy()
this.clusterVueInstance = null
}
this.clusterVueInstance = new Vue({ template: '<tbody>'+template+'</tbody>' })
//or use Vue.extend()
this.clusterVueInstance.$slots = this.$slots
this.clusterVueInstance.$scopedSlots = this.$scopedSlots
this.clusterVueInstance.$mount()
this.clusterize.content_elem.innerHTML = this.clusterVueInstance.$el.innerHTML
//console.log(this.clusterVueInstance.$scopedSlots); // empty
//console.log(this.clusterVueInstance.$slots) // not empty*/
};
}
})
app = new Vue({
el: "#app",
data() {
return {
test: ['Puss In Boots', 'test 1', 'test2'],
index: 0
}
},
mounted: function () {
//this.test = ['Puss In Boots', 'test 1', 'test2']
},
methods: {
addItem: function () {
this.test.push(`test ` + this.index++)
}
}
})
<link href="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.js"></script>
<div id="app">
<button #click="addItem()">
Add Item
</button>
<clusterize :items="test">
<template slot-scope="props">
item: {{props.item}}
</template>
</clusterize>
</div>
Please look into below demo: created multiple Vue instance to same el, but Vue always uses first instance to render (I can't find any useful statement at Vue Guide, probably from the source codes from Vue Github we can find out the logic. If someone knows, please feel free to edit my answer or add a comment).
Vue.config.productionTip = false
app1 = new Vue({
el: '#app',
data () {
return {
test: 'test 1'
}
},
mounted(){
console.log('app1', this.test)
}
})
app2 = new Vue({
el: '#app',
data () {
return {
test: 'test 2'
}
},
mounted(){
console.log('app2', this.test)
}
})
//app1.$data.test = 3
//app1.$mount() //manual mount
app2.$data.test = 4
app2.$mount() //manual mount
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/clusterize.js/0.18.0/clusterize.min.js"></script>
<div id="app">
<a>{{test}}</a>
</div>
Related
Hi i want to wrap the content of a component with some specific html tag let say button for this example.
i have a function which dynamically returns a value which i use as a prop, based on that i want to wrap the content of a component.
i know i could have achieved this way too <button><compA/></button> it does not solve my problem beacuse i need to change it in 100 places.
My expected result:
<button><div>press me i'm button</div></button>
<div>don't wrap me with button leave me as it is</div>
Note: :wrappwithbutton="" having true for 1st usage and false for 2nd usage
const localComponent = {
name:'first-comp',
template:`<div> {{text}}</div>`,
props:['wrappwithbutton','text'],
}
const app = new Vue({
el:'#app',
name:'app',
components:{'first-comp':localComponent},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<first-comp :wrappwithbutton="true" text="press me i'm button"></first-comp>
<br/>
<hr/>
<br/>
<first-comp :wrappwithbutton="false" text="don't wrap me with button leave me as it is"></first-comp>
</div>
This is a perfect example for render functions. Instead of using a template you can use a render function to render the template for you. Read more about render functions
const localComponent = {
name:'first-comp',
props:['wrappwithbutton', 'text'],
methods: {
btnClick() {
if (this.wrappwithbutton) console.log('button')
}
},
render(h) {
return h(this.wrappwithbutton ? 'button' : 'div', [
h('div', this.text)
])
}
}
const app = new Vue({
el:'#app',
name:'app',
components:{'first-comp':localComponent},
});
Vue.config.productionTip = false
Vue.config.devtools = false
You can even go a step further and make your localComponent to be more dynamic with the parent passing a prop with the tag that should be rendered:
const localComponent = {
name:'first-comp',
props:['tag', 'text'],
methods: {
btnClick() {
if (this.wrappwithbutton) console.log('button')
}
},
render(h) {
return h(this.tag, [
h('div', this.text)
])
}
}
If you would like to have a single div and not two divs you can do:
render(h) {
if (this.tag === 'div') {
return ('div', this.text);
}
return h(this.tag ? 'button' : 'div', [
h('div', this.text)
])
}
This is my idea, but I think the template should have a more concise way of writing
const localComponent = {
name: "first-comp",
template: `
<template v-if="wrappwithbutton">
<button>
<div> {{text}}</div>
</button>
</template>
<template v-else>
<div> {{text}}</div>
</template>
`,
props: ["wrappwithbutton", "text"]
};
const app = new Vue({
el: "#app",
name: "app",
components: { "first-comp": localComponent }
});
So I have this set-up, which crashes on v-for construct of table-component. It shows an error: "Property or method "tablesList" is not defined on the instance but referenced during render". If I omit v-for table-component renders. If I access this data from "container" component all is fine. So the problem is in accessing data from child template in parent template.
What am I doing wrong?
let container = Vue.component("container", {
props: ["item"],
template: `<div class="container">
<table-component v-for="item in tablesList"></table-component>
</div>`
});
let table = Vue.component("table-component", {
props: ["item"],
template: `<div class="table">
this is a table
</div>`
});
let app = new Vue({
el: "#app",
data() {
return {
containersList: [],
tablesList: [{item:'item'}]
};
},
methods: {
anyMethod() {
}
}
});
</script>
You are using tablesList in container component But you defined it in app.
You need to add tablesList in container like below,
let container = Vue.component("container", {
props: ["item"],
data: () => {
return {
tablesList: [{item:'item'}]
}
},
template: `<div class="container">
<table-component v-for="item in tablesList"></table-component>
</div>`
});
NOTE: Use v-bind:key when use v-for.
You need to define tablesList in props => https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props
I want to use slots in Vue to create a dynamic modal component.
I already tried a lot of tutorials of Vue / slots, but none of them is exactly what i'm looking for.
This is a piece of my modal.vue:
<template>
...
<slot name="modal-body"></slot>
...
</template>
<script>
</script>
<style>
</style>
This is my javascript compiled file:
import Vue from 'vue';
import modal from './modal.vue';
new Vue({
el: '#modal',
render: r => r(modal)
});
This is piece of my HTML file:
...
<div id="modal">
<template v-slot="modal-body">
<input type="text" id="dynamic-input">
</template>
</div>
...
I was expecting that all elements present inside #modal (#dynamic-input in this case), were inserted into the slot named modal-body, inside my Vue element.
Is it possible to do it? Am i missing something?
Check what version of Vue you are using. The named slot syntax changed in 2.6.0. Consider the differences below. One uses render functions and the other template Strings.
Vue#2.6.10
// Example using template String
const modalTemplateString = {
template: "<div><div>above</div><slot name=\"modal-body\"></slot><div>below</div></div>"
};
const appTemplateString = new Vue({
el: "#appTemplateString",
components: {
modal: modalTemplateString
},
template: "<div><modal><template v-slot:modal-body><div>foobar</div></template></modal></div>"
});
// Example using render function
const modalRenderFunc = {
render(h) {
return h("div", [
h("div", "above"),
h("div", this.$slots["modal-body"]),
h("div", "below")
]);
}
}
const appRenderFunc = new Vue({
el: "#appRenderFunc",
components: {
modal: modalRenderFunc
},
render(h) {
return h("div", [
h("modal", [
h("div", {
slot: "modal-body"
}, "foobar")
])
]);
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<h2>Template String</h2>
<div id="appTemplateString"></div>
<hr/>
<h2>Render Function</h2>
<div id="appRenderFunc"></div>
Vue#2.5.22
// Example using template String
const modalTemplateString = {
template: "<div><div>above</div><slot name=\"modal-body\"></slot><div>below</div></div>"
};
const appTemplateString = new Vue({
el: "#appTemplateString",
components: {
modal: modalTemplateString
},
template: "<div><modal><template slot=\"modal-body\"><div>foobar</div></template></modal></div>"
});
// Example using render function
const modalRenderFunc = {
render(h) {
return h("div", [
h("div", "above"),
h("div", this.$slots["modal-body"]),
h("div", "below")
]);
}
}
const appRenderFunc = new Vue({
el: "#appRenderFunc",
components: {
modal: modalRenderFunc
},
render(h) {
return h("div", [
h("modal", [
h("div", {
slot: "modal-body"
}, "foobar")
])
]);
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.22/dist/vue.js"></script>
<h2>Template String</h2>
<div id="appTemplateString"></div>
<hr/>
<h2>Render Function</h2>
<div id="appRenderFunc"></div>
I'm trying to use a statement within a element:
v-if="currentstep < maxStep"
The maxStep should be obtained from the number of components listed on my defauld export
export default {
name: 'step',
data () {
return {
maxStep: 8,
currentstep: 0
}
},
components: {
ConfigPublicador,
ConfigServico,
ModeloReceita,
Integracoes,
ConfigTema,
ConfigApp,
ConfigExtras,
Assets,
Revisao
}
}
Something like
maxStep = components.length
Any Ideias?
Thanks
This is definitely a code smell. But, you can get that value via Object.keys(this.$options.components).length.
Here's an example:
const Foo = {
template: '<div></div>',
}
new Vue({
el: '#app',
components: { Foo },
data() {
return {
count: 0,
}
},
created() {
this.count = Object.keys(this.$options.components).length;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.js"></script>
<div id="app">
<div v-if="count > 0">
There is at least one component
</div>
</div>
I have a vue component for simple pagination of a table. I need to highlight a row of said table on mouse over. What I did so far is create the template and create the dynamically loaded ajax powered next and previous buttons. The issue is that highlighting because its a dynamic element.
Here is the template code:
<tbody class="table-body">
<template id="template-student">
<tr #mouseover="highlightRow()" class="students-row" data-student-url="{{ URL::route("student", 1) }}">
<td>
<div class="student-image">
<div class="flag"></div>
</div>
</td>
<td>
#{{ student.student_firstname }}
</td>
<td>
#{{ student.student_lastname }}
</td>
<td>
#{{ student.student_telephone }}
</td>
<td>
#{{ student.student_fathersname }}
</td>
</tr>
</template>
</tbody>
And the vue code:
Vue.component('student', {
template: '#template-student',
props: ['student'],
});
new Vue({
el: '#app',
data: {
students: [],
pagination: {}
},
ready() {
this.fetchStudents();
},
methods: {
fetchStudents(pageNumber) {
if (pageNumber === undefined) {
pageNumber = 1;
}
const vm = this;
const pageUrl = `/api/on-roll/all/${pageNumber}`;
this.$http.get(pageUrl)
.then((response) => {
vm.makePagination(response.data.pagination);
vm.$set('students', response.data.data);
});
},
makePagination(data) {
const pagination = {
current_page: data.current_page,
total_pages: data.total_pages,
next_page: data.next_page,
prev_page: data.prev_page
};
this.$set('pagination', pagination);
},
highlightRow(){
console.log("test")
}
}
});
The issue is when I hover over any row I get an error:
Uncaught TypeError: scope.highlightRow is not a function
Now I understand (more or less) why is this happening, I am calling a function inside the template tags (if I cal outside on a example element it works). Some research lead me to this question dynamical appended vuejs content: scope.test is not a function But the help there didn't help me. I couldn't figure out how to implement the solution to my example.
Any time you want a component to have access to something from a parent, pass it in as a prop.
new Vue({
el: '#app',
components: {
myComponent: {
template: '<div #mouseover="onHover">Hi</div>',
props: ['onHover']
}
},
methods: {
highlightRow() {
console.log('test');
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<my-component :on-hover="highlightRow"></my-component>
</div>