I want to get a third party library component, add one element more and use this third party with the same way as always.
Example:
<third-party foo="bar" john="doe" propsFromOriginalLibrary="prop">
<template v-slot:top=v-slot:top={ props: test }>
Some text on third-party component slot
</template>
</third-party>
Want to code as:
<my-custom-component propsFromOriginalLibrary="prop">
<template v-slot:top={ props: test }>
Some text on third-party component slot
</template>
</my-custom-component>
And both examples work the same way. I was able to get all the props by using:
<third-party v-bind="$attrs">
but not sure about how to handle the slots
Here is how you can do it:
my-custom-component template:
<template>
<third-party v-bind="$attrs">
<template v-slot:top="slotProps">
<slot name="top" v-bind="slotProps"></slot>
</template>
</third-party>
</template>
What's happening:
Inserting the third party component and v-bind $attrs
Referencing its top slot
Custom component has a slot which is passed into third's slot
Custom slot has the same name so it can be v-slot the same way from a parent
Custom slot v-binds all 3rd party slotProps to pass out to a parent
You can use a v-for to avoid the need for hard-coding an inner template for each slot. For example if you wanted to expose two slots, top and bottom:
<template>
<third-party v-bind="$attrs">
<template v-for="slot in ['top','bottom']" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps"></slot>
</template>
</third-party>
</template>
Demo:
Vue.component('third-party', {
props: ['background'],
template: `
<div class="third" :style="{ background }">
3rd party slot:
<div class="third-slot">
<slot name="top" :props="props"></slot>
</div>
</div>
`,
data() {
return {
props: 'Third party Prop'
}
},
})
Vue.component('my-custom-component', {
template: `
<div>
<component is="third-party" v-bind="$attrs">
<template v-for="slot in ['top']" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps"></slot>
</template>
</component>
</div>
`
})
/***** APP *****/
new Vue({
el: "#app"
});
.third,.third-slot {
padding: 10px;
}
.third-slot {
background: #cccccc;
border: 1px solid #999999;
font-weight: bold;
}
<script src="https://unpkg.com/vue#2.6.12/dist/vue.js"></script>
<div id="app">
<third-party background="#eeeeff">
<template v-slot:top="{ props: test }">
Some text on third-party component slot
<div>{{ test }}</div>
</template>
</third-party>
<my-custom-component background="red">
<template v-slot:top="{ props: test }">
Some text on third-party component slot
<div>{{ test }}</div>
</template>
</my-custom-component>
</div>
Fun: You could even make the wrapped component dynamic like <component :is="thirdpartyName"> and the slot name array too; even passing this info in from outside for a fully generic wrapper. But there's no need for that here
Related
I have a wrapper component(Wrapper.vue).
<component v-for="c of comps" :key="c.componentName" :is="c.componentName">
<slot :name="c.componentName">
</slot>
</component>
const comps =
[
{
componentName: "Child1",
},
{
componentName: "Child2",
}
];
Child1.vue is something like below
<template>
<div class="hello">
<slot name="top">Child1 slot top text</slot>
</div>
</template>
Child2.vue is something like below
<template>
<div class="hello">
<slot name="top">Child2 slot top text</slot>
</div>
</template>
I want to use the wrapper like below in App.vue
<Wrapper>
<template slot="slot1">
<template slot="top">Inner Content1</template>
</template>
<template slot="slot2">
<template slot="top">Inner Content2</template>
</template>
</Wrapper>
CodeSandbox Link:- https://codesandbox.io/s/mystifying-burnell-ir8un
It is showing default text from Child1 and Child2 not from App.vue
What should be done to fix it?
I changed some code and this is the result. The changes I made:
you had nested the <template> tag inside App.vue.
the wrapper itself needed to have a template for slot named 'top' and inside this template I added a slot with the same name of the component
https://codesandbox.io/s/lucid-smoke-uzgn7?file=/src/App.vue
I'm implementing an application with Vue Js and I've the following code:
<template>
<simple-page title="list-patient" folder="Patient" page="List Patient" :loading="loading">
<list-patients #patientsLoaded="onPatientsLoaded"/>
</simple-page>
</template>
Both simple-page and list-patients are custom components created by me. Inside ListPatients I've an HTTP request on Create callback, as follows:
created() {
axios.get("...").then(response => {
...
this.$emit('patientsLoaded');
})
},
Then, my objective is to handle the patientsLoaded event and uptade the loading prop on the top parent component, as follows:
data() {
return {
loading: true
}
},
methods: {
onPatientsLoaded(params) {
this.loading = false;
}
}
However, the created method is not being triggered inside the list-patients component. The only way I can make this work is by removing :loading.
Any one can help?
Edit 1
Code of simple page:
<template>
<section :id="id">
<!-- Breadcrumb-->
<breadcumb :page="page" :folder="folder"/>
<!-- Breadcrumb-->
<!-- Simple Card-->
<simple-card :title="page" :icon="icon" :loading="loading" v-slot:body>
<slot>
</slot>
</simple-card>
<!-- Simple Card-->
</section>
</template>
Code of simple card:
<b-card>
<!-- Page body-->
<slot name="body" v-if="!loading">
</slot>
<!--Is loading-->
<div class="loading-container text-center d-block">
<div v-if="loading" class="spinner sm spinner-primary"></div>
</div>
</b-card>
Your list-patients component goes in the slot with name "body". That slot has a v-if directive so basically it is not rendered and hooks are not reachable as well. Maybe changing v-if to v-show will somehow help you in that situation. Anyway, you have deeply nested slots and it is making things messy. I usually declare loading variable inside of the component, where fetching data will be rendered.
For example:
data () {
return {
loading: true;
};
},
mounted() {
axios.get('url')
.then(res => {
this.loading = false;
})
}
and in your template:
<div v-if="!loading">
<p>{{fetchedData}}</p>
</div>
<loading-spinner v-else></loading-spinner>
idk maybe that's not best practise solution
v-slot for named slots can be indicated in template tag only
I suppose you wished to place passed default slot as body slot to simple-card component? If so you should indicate v-slot not in simple-card itself but in a content you passed it it.
<simple-card :title="page" :icon="icon" :loading="loading">
<template v-slot:body>
<slot>
</slot>
</template>
</simple-card>
I'm attempting to create components using Vue, so that I can remove a lot of duplicated HTML in a site I'm working on.
I have a <ym-menucontent> component, which within it will eventually have several other components, conditionally rendered.
While doing this I've hit a wall and so have simplified everything to get to the root of the problem.
When rendering the ym-menucontent component the first sub-component is the only one which gets rendered and I can't work out why or how to get around it...
<template id="menucontent">
<div>
<ym-categories :menuitem="menuitem"/>
<ym-rootmaps :menuitem="menuitem"/>
<p>1: {{menuitem.rootMapsTab}}</p>
<p>2: {{menuitem.exploreTab}}</p>
</div>
</template>
<template id="rootmaps">
<div>Root Maps</div>
</template>
<template id="categories">
<div>Categories</div>
</template>
app.js
Vue.component('ym-menucontent', {
template: '#menucontent',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON
}
}
});
Vue.component('ym-rootmaps', {
template: '#rootmaps',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON,
rootMaps: window.rootAreas
}
}
});
Vue.component('ym-categories', {
template: '#categories',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON,
rootMaps: window.rootAreas
}
}
});
usage...
<div
v-for="mi in customMenu.topLevelMenuItems"
:id="mi.name"
class="page-content tab swiper-slide">
<ym-menucontent :menuitem="mi"/>
</div>
Output
<div>Categories</div>
if I switch around ym-cateogries and ym-rootmaps then the output becomes...
<div>Root Maps</div>
if I remove both then I see...
<p>1: true</p>
<p>2:</p>
I'd expect to see a combination of all of them...
<div>Categories</div>
<div>Root Maps</div>
<p>1: true</p>
<p>2:</p>
This is probably because you're using self-closing components in DOM templates, which is recommended against in the style-guide ..
Unfortunately, HTML doesn’t allow custom elements to be self-closing -
only official “void” elements. That’s why the strategy is only
possible when Vue’s template compiler can reach the template before
the DOM, then serve the DOM spec-compliant HTML.
This should work for you ..
<template id="menucontent">
<div>
<ym-categories :menuitem="menuitem"></ym-categories>
<ym-rootmaps :menuitem="menuitem"></ym-rootmaps>
<p>1: {{menuitem.rootMapsTab}}</p>
<p>2: {{menuitem.exploreTab}}</p>
</div>
</template>
<div
v-for="mi in customMenu.topLevelMenuItems"
:id="mi.name"
class="page-content tab swiper-slide">
<ym-menucontent :menuitem="mi"></ym-menucontent>
</div>
I've noticed that transition hooks only get fired when the <transition> element is the root element in my component template. Is this by design? Am I missing something?
in my App.vue I have this template:
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<example v-if = "checked"></example>
My component example.vue:
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</section>
</template>
<script lang="js">
export default {
name: 'example',
props: [],
mounted() {
},
data() {
return {
}
},
methods: {
enter: function (el, done) {
console.log("enter")
done()
},
leave: function(el, done) {
console.log("leave")
done()
}
}
}
</script>
<style scoped >
</style>
In this current exmaple the enter and leave hooks are never executed when toggling the checkbox.
If I would update the template of example.vue to make sure the <transitions> element is the root element (as shown below) the enter and leave hooks are called.
<template lang="html">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</template>
I'd like to have more flexibility in where I put my <transition> element or have multiple transition element in a component, which all have their own hooks.
I'm assuming I am overlooking something that prevents me from doing this.
I've noticed that transition hooks only get fired when the element is the root element in my component template. Is this by design? Am I missing something?
It's because of this line <example v-if="checked"></example>. v-if is applied to real root element of component so when transition is in root, v-if applied to div inside transition and it works fine, but in your first case v-if applied to section which is not under transition. So to make transition work you should provide v-if in element wrapped with transition tag, you can pass checked as prop to indicate visibility:
App.vue
...
<example :visible="checked"></example>
...
Example.vue
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div v-if="visible" class="transition-example"></div>
</transition>
</section>
</template>
I have a Vue component simplified below.
Here is the template
<template>
<slot></slot>
</template>
The slot may contain HTML, which is why I decided to use a slot rather than a prop which I would simply bind to. I'd like to keep it that way.
I have a method that gets new HTML from the server. I'd like to use this new HTML to update the slot. I'm not sure if slots are reactive and how I can accomplish this.
I can view the default slot using this.$slots.default[0], but I don't know how to update it with a string of HTML content. Simply assigning the string to the element is obviously incorrect, to .innerHtml does not work because it isn't an available function, and to .text doesn't work. I assume that even though the text element exists on the slot object, the element properties take precedence.
Per suggestion in comments, I've tried this along with a computer property.
<span v-html="messageContent"><slot></slot></span>
But now the problem is that it overwrites the slot passed to me.
How can I reactively update a slot with new HTML in Vue.JS?
I think your issue comes from a misunderstanding of how <slot> inherently works in VueJS. Slots are used to interweave content from a consuming parent component into a child component. See it as a HTML equivalent of v-bind:prop. When you use v-bind:prop on a component, you are effectively passing data into a child component. This is the same as slots.
Without any concrete example or code from your end, this answer is at best just guess-work. I assume that your parent component is a VueJS app itself, and the child component is the one that holds the <slot> element.
<!-- Parent template -->
<div id="app">
<custom-component>
<!-- content here -->
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
In this case, the app has a default ground state where it passes static HTML to the child component:
<!-- Parent template -->
<div id="app">
<custom-component>
<!-- Markup to be interweaved into custom component -->
<p>Lorem ipsum dolor sit amet.</p>
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
Then, when an event is fired, you want to replace that ground-state markup with new incoming markup. This can be done by storing the incoming HTML in the data attribute, and simply using v-html to conditionally render it. Let's say we want to store the incoming markup in app's vm.$data.customHTML:
data: {
customHTML: null
}
Then your template will look like this:
<!-- Parent template -->
<div id="app">
<custom-component>
<div v-if="customHTML" v-html="customHTML"></div>
<div v-else>
<p>Lorem ipsum dolor sit amet.</p>
</div>
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
Note that in contrast to the code you have tried, the differences are that:
It is the parent component (i.e. the consuming component) that is responsible for dictating what kind of markup to pass to the child
The child component is as dumb as it gets: it simply receives markup and renders it in the <slot> element
See proof-of-concept below:
var customComponent = Vue.component('custom-component', {
template: '#custom-component-template'
});
new Vue({
el: '#app',
data: {
customHTML: null
},
components: {
customComponent: customComponent
},
methods: {
updateSlot: function() {
this.customHTML = '<p>Foo bar baz</p>';
}
}
});
.custom-component {
background-color: yellow;
border: 1px solid #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<h1>I am the app</h1>
<button type="button" #click="updateSlot">Click me to update slot content</button>
<custom-component>
<div v-if="customHTML" v-html="customHTML">
</div>
<div v-else>
<p>Lorem ipsum dolor sit amet.</p>
</div>
</custom-component>
</div>
<!-- custom-component template -->
<script type="text/template" id="custom-component-template">
<div class="custom-component">
<h2>I am a custom component</h2>
<!-- slot receives markup set in <custom-component> -->
<slot></slot>
</div>
</script>
Below is my solution though I don't like this opinion (load html into slot directly in current component level) because it breaks the rules for the slot. And I think you should do like this way (<component><template v-html="yourHtml"></template></component>), it will be better because Slot will focus on its job as Vue designed.
The key is this.$slots.default must be one VNode, so I used extend() and $mount() to get the _vnode.
Vue.config.productionTip = false
Vue.component('child', {
template: '<div><slot></slot><a style="color:green">Child</a></div>',
mounted: function(){
setTimeout(()=>{
let slotBuilder = Vue.extend({
// use your html instead
template: '<div><a style="color:red">slot in child</a></div>',
})
let slotInstance = new slotBuilder()
this.$slots.default = slotInstance.$mount()._vnode
this.$forceUpdate()
}, 2000)
}
})
new Vue({
el: '#app',
data() {
return {
test: ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<child><h1>Test</h1></child>
</div>