How to use slots in the HTML with Single File Components - javascript

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>

Related

how to wrap a component content with html tag dynamically in vue

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 }
});

Vue.js does not update HTML

I have the following .html file in my local Vue.js project (it's a simplified version):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue.js</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<main>
<div id="myElement">
<div v-for="(properties, name) in list" v-bind:key="name">{{ name }}</div>
</div>
</main>
<script type="module">
import SomeClass from "./SomeClass.js";
const myClass = new SomeClass();
let app = new Vue({
el: "#myElement",
data: {
list: myClass.object
},
});
</script>
</body>
</html>
Inside the <script type="module"> tag I import SomeClass which includes a property called object:
export default class SomeClass {
constructor() {
this.object = {
name1: { ... },
name2: { ... },
...
};
}
}
The issue here is that Vue.js reacts to changes in object (if I check it using watch) but it does not update my HTML:
<div v-for"..." v-bind:key="...">...</div>.
What can I do to make Vue.js update my HTML when new properties added to object / old properties updated?
Vue has a method to add properties to items in the data attribute (so they keep reactivity). (More on this: https://v2.vuejs.org/v2/api/#Vue-set)
The snippet below may help you to see the method in action:
class SomeClass {
constructor() {
this.object = {
name1: {
name: 'name3'
},
name2: {
name: 'name4'
}
}
}
}
const myClass = new SomeClass()
new Vue({
el: "#app",
computed: {
// transforming array for reactive display
transformedObj() {
const ret = []
this.keyVal().forEach(e => {
e.forEach(el => {
ret.push(el)
})
})
return ret
}
},
data: {
object: myClass.object
},
methods: {
// transforming object for better display
keyVal() {
return Object.keys(this.object).map(e => {
return Object.entries(this.object[e]).map(([k, v]) => {
return `${k}: ${v}`
})
})
}
},
mounted() {
// add new property (reactive)
Vue.set(this.object.name1, 'newProperty', '2')
// or add new property (reactive)
this.$set(this.object.name2, 'localNewProperty', 45)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="(item, key) in transformedObj">{{item}}</li>
</ul>
Object: {{transformedObj}}
</div>

Vue.js $scopedSlots don't work for Vue instance

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>

How to pass data to nested child components vue js?

I get how to pass data from parent to child with props in a situation like:
<template>
<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: stuff,
props: ['content'],
data () {
return {
}
}
}
</script>
And then bind the data to the component in the parent component like,
<template>
<div>
<stuff v-bind:content="stuffToPass"></stuff>
</div>
</template>
<script>
import stuff from './stuff.vue';
export default {
data () {
return {
stuffToPass: [
{id: 1, items: 'foo'},
{id: 2, items: 'bar'},
{id: 3, items: 'baz'}
]
}
},
components: {
stuff
}
}
</script>
But say I have the root component, and I want to pass data to the stuff component, like in the above, but when I have a number of other components like parent > x > y > stuff, and it's still the stuff component that will ultimately be receiving that data, I don't know how to do that.
I heard of provide/inject, but I'm not sure that's the appropriate use, or at least I couldn't get it working.
Then I tried passing props, but then I found myself trying to bind a prop to a component to pass as a prop to a child component and that doesn't sound right, so then I just re-wrote my components in the 'stuff' component, but I feel that's probably re-writing way to much code to be close to reasonable.
there are a few possibilities to pass data parent > x > y > stuff
props - applicable but you would have to pipe the data through all components...
store (vuex) - applicable but could become complicated to handle
event bus - the most flexible and direct way
below, a simple example on how to implement the event bus:
// src/services/eventBus.js
import Vue from 'vue';
export default new Vue();
the code from where you want to emit the event:
// src/components/parent.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
methods: {
eventHandler(val) {
EventBus.$emit('EVENT_NAME', val);
},
},
...
};
</script>
the code for where you want to listen for the event:
// src/components/stuff.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
mounted() {
EventBus.$on('EVENT_NAME', val => {
// do whatever you like with "val"
});
},
...
};
</script>
Use watchers or computed properties https://v2.vuejs.org/v2/guide/computed.html
const Stuff = Vue.component('stuff', {
props: ['content'],
template: `<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>`
});
const Adapter = Vue.component('adapter', {
components: { Stuff },
props: ['data'],
template: `<div>
<Stuff :content="newData"/>
</div>`,
data() {
return {
newData: []
};
},
created() {
this.changeData();
},
watch: {
data: {
deep: true,
handler: function() {
this.changeData();
}
}
},
methods: {
changeData() {
this.newData = JSON.parse(JSON.stringify(this.data));
}
}
});
const app = new Vue({
el: '#app',
components: { Adapter },
data() {
return {
stuffToPass: [
{ id: 1, items: 'foo' },
{ id: 2, items: 'bar' },
{ id: 3, items: 'baz' }
]
};
},
methods: {
addItem() {
this.stuffToPass.push({ id: this.stuffToPass.length + 1, items: 'new' });
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.1/vue.js"></script>
<div id="app">
<button #click="addItem">Add</button>
<Adapter :data="stuffToPass"/>
</div>

Vue: Access component object properties

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>

Categories

Resources