Multiple templates declared in one .vue file - vue.js

I have a component inside a .vue file that can benefit from reusing a chunk of code. I know I can move that code to a separate .vue file and import it as a new component. However, this component would not be used anywhere else and I'd like to avoid cluttering the directory. Is it possible to declare this component's template inside the parent without using the in-code template:"<div>.....</div>" stuff?
This is the idea:
<template>
<div>
...some html here...
<div v-for="item in items">
{{item.name}}:
<div v-if="item.available">YES!</div>
<div v-else>NO :(</div>
</div>
...some other components and data here...
<div v-for="item in items">
{{item.name}}:
<div v-if="item.available">YES!</div>
<div v-else>NO :(</div>
</div>
</div>
</template>
I would like to be able to do something like this:
<template>
<div>
...some html here...
<div v-for="item in items">
<itemizer inline-template v-model="item">
{{value.name}}:
<div v-if="value.available">YES!</div>
<div v-else>NO :(</div>
</itemizer>
</div>
...some other components and data here...
<div v-for="item in items">
<itemizer v-model="item"/>
</div>
</div>
</template>
However, from what I understand this is not possible.

Unfortunately this pattern does not appear to be supported by the creator of Vue:
I personally feel the syntax is less maintainable than [Single File Components]
Note that we want to keep the SFC syntax as consistent possible, because it involves a big ecosystem of tools that need to support any new features added (e.g. Vetur would need to do something very different for handling SFCs with multiple scripts/templates). The proposed syntax, IMO, does not justify adding the new syntax.
https://github.com/vuejs/vue/pull/7264#issuecomment-352452213
That's too bad, as even beyond flexibility and developer choice, there is a good argument for inlining small functions that are not used by other components in order to reduce complexity. It's a common pattern in React and does not inhibit Single File Components when they're needed. In fact it allows gradual migration as inline components grow.
Here's one of the only resources currently that offers some potential workarounds:
https://codewithhugo.com/writing-multiple-vue-components-in-a-single-file/
I've tried them all and am not satisfied with any of them at this time. At best you can set runtimerCompiler: true and use template strings, but it'll add 10KB to your bundle and you'll likely miss out on full syntax highlighting available in the <template> element. Maybe you can hack Teleport, but I have not made a dedicated attempt.

Actually, this should work. Just register your Vue inline-template like this in the section of your parent .vue file:
<template>
<div v-for="item in items">
<test-template :item="item">
<h1>{{item.text}}</h1>
</test-template>
</div>
</template>
<script>
Vue.component('test-template', {
template:'#hello-world-template',
props: {
item: Object
}
});
export default {...}
</script>
In your parent HTML file, put this:
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>

With vue3 there are multiple options:
with vue-jsx you can just declare a component in your script setup section and use that
const Named = defineComponent(() => {
const count = ref(0)
const inc = () => count.value++
return () => (
<button class="named" onClick={inc}>
named {count.value}
</button>
)
})
There is another option described by Michael Thiessen here
Also you can have multiple render function components in one file:
https://staging.vuejs.org/guide/extras/render-function.html

Although it is not supported in Vue core yet, there is a way to use this through vue macros project. See discussion here

Related

How to properly create a popup component in Vue 3

As part of becoming a better Vue programmer, I am trying to implement a popup similar to Popper with a clean and Vueish architecture. Here is a simple schematic that I came up with:
So basically there is a target component, which is the reference for the popup's position. The popup can be positioned above, below, right and left of the target, therefore I will need to have access to the target element in my popup. Also, the target can be an arbitrary component. It can be a simple button or span, but also something much more complex.
Then there is the popup itself, which will be put into a modal at the end of the body, It contains the actual content. The content again can be an arbitrary component.
I have a working implementation of the popup, but the basic structure seems to be far from perfect. I am using two slots, one for the target element and one for the content.
Here is what I have come up with so far for the template:
<template>
<div ref="targetContainer">
<slot name="target"></slot>
</div>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot name="content"></slot>
</div>
</div>
</teleport>
</template>
There are several issues with this that I am not really happy with.
Using the popup is not very simple
When using this popup in another component, two <template> tags are rquired. This is ungly and not very intuitive. A very simple use case looks like this:
<modal :show="showPopup" #close="showPopup=false">
<template v-slot:target>
<button #click="showPopup=true"></button>
</template>
<template v-slot:content>
<div>Hello World!</div>
</template>
</modal>
The target is wrapped in another <div>
This is done to get access to the target element, that I need for the layout. In mounted() I am referencing the target element like this:
let targetElement = this.$refs.targetContainer.children[0];
Is this really the best way to do this? I would like to get rid of the wrapping <div> element, which just asks for unintended side effects.
The best solution would be to get rid of one slot and somehow reference the target element in another way because I only need its layout information, it does not have to be rendered inside the popover component.
Can someone point me in the right direction?
Here is my solution, which was inspired by a comment on my question and which I think is worth sharing.
Instead of putting the target element into a slot, I am now passing its ref as a prop, which makes things much cleaner.
The popover component's template now looks like this.
<template>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot ref="content"></slot>
</div>
</div>
</teleport>
</template>
I has a targetRefprop, so the component can be simply used like this:
<div ref="myTargetElement" #click="isPopupVisible=true">
</div>
<modal :show="isPopupVisible" #close="isPopupVisible=false" targetRef="myTargetElement">
<!-- popup content goes here -->
</modal>
And after mounting I can access the target element like this:
let targetElement = this.$parent.$refs[this.targetRef];
I like this solution a lot. However, ideas, advice or words of caution are still highly welcome.

What does it actually means - Use created + activated for keep-alive components in Vuejs

Initially, I am fetching data from api in the created hook which is perfectly working.
created() {
this.fetchInformation()
}
But I was having look over best practices for lifecycle hooks and I came to this line You need to fetch some data for your component on initialization. Use created (or created + activated for keep-alive components)
I also tried to look for relevant articles or information on the internet.
Url for reference - https://alligator.io/vuejs/component-lifecycle/
My component is rendering inside keep-alive so I tried this for the test purpose.
activated() {
this.fetchInformation()
}
Instead of created, now as expected everytime the component activates it execute the api call which is really cool. But I still want to understand what this actually created + activated as I am using activated or created but if I am correct just by reading that I should do them both.
Please let me know if anything else required to understand my question.
Thanks
Use correctly keep-alive!!
Incorrect:
<template>
<div>
<div v-if="canRender">
<keep-alive>
<my-component />
</keep-alive>
</div>
</div>
<template>
Incorrect:
<template>
<div>
<keep-alive>
<div v-if="canRender">
<my-component />
</div>
</keep-alive>
</div>
<template>
Correct:
<template>
<div>
<div>
<keep-alive>
<my-component v-if="canRender" />
</keep-alive>
</div>
</div>
<template>

How to let a VueJS component work only as a wrapper instead of having control of all the html?

In other words one might have an html at some point which looks like this:
<bloglink blogposturl="http://a.link" blogposttitle="my title" ></bloglink>
And the template attribute like so:
...
template: '<h3><i><a style="text-decoration: underline; cursor: pointer;" v-on:click="load_blog_page(blogposturl)">{{blogposttitle}}</a></i></h3>',
...
But what if one wanted to express instead
<bloglink blogposturl="http://a.link" blogposttitle="my title" >
<p>.....complex html in here.... which is not dynamic....</p>
</bloglink>
One would want to keep this html when loading the webpage instead of being completely replaced.
One way would be to take this complex html and insert it as a parameter being careful with escaping properly etc. but this does not seem very elegant.
What is the recommended way?
Vue component slot.
for example:
ParentComponent:
<template>
<div>
<slot></slot>
</div>
</template>
SomeOtherComponent:
<template>
<div>
<parent>
<p>whatever</p>
</parent>
</div>
</template>

vue.js Mount component to app root

I have a modal.vue component as follows:
<template>
<transition name="modal-transition">
<div class="modal-body" v-if="displayed">
<div class="modal-overlay" #click="displayed = false"></div>
<div class="modal-content">
<slot/>
</div>
</div>
</transition>
</template>
How do I mount this component to the applications root element rather than in place?
For crude inaccurate example:
<body>
<div id="app">
<div class="header"></div>
<div class="nav"></div>
<div class="stage">
<div class="sub-nav"></div>
<div class="content">
<modal :display.sync="display">MY MODAL</modal> <-- Don't mount here...
</div>
</div>
<-- Mount here instead...
</div>
</body>
The current issue is that my sites header and navigation is layered on top of my modal and it's darkened full screen overlay instead of layered behind the modal overlay.
Update for Vue 3
There is now a built in feature called teleport which allows mounting parts of your component template to any DOM element.
The example from the OP would look like something like this
<!-- MyModal.vue -->
<template>
<transition name="modal-transition">
<div class="modal-body" v-if="displayed">
<div class="modal-overlay" #click="displayed = false"></div>
<div class="modal-content">
<slot/>
</div>
</div>
</transition>
</template>
<!-- SomeDeeplyNestedComponent.vue -->
<template>
<teleport to="#app">
<!-- Can still receive props from parent -->
<MyModal :my-prop="foo">
<!-- slot content -->
</MyModal>
</teleport>
</template>
Vue 2
Move the elements own self to the element of applications root may be achieved in two ways, Using a portal as a preferred solution or using an append.
Using a Portal (Preferred Method)
PortalVue is a set of two components that allow you to render a
component's template (or a part of it) anywhere in the document - even
outside the part controlled by your Vue App!
https://portal-vue.linusb.org/
Using an Append (Not best practice)
If adding a portal library is too heavy, using an append is allowed but lightly discouraged officially in the VUE docs.
Typically this particular mount position will satisfy a z-index overlay for your own modal or dialog popup that you require to render over the top of the entire app. You can always substitute this.$root.$el in this example for a different element target using standard getElementBy or querySelector functions.
Here the element is being moved not destroyed and re-added, all reactive functionality will remain in tact.
<script>
export default {
name: 'modal',
...
mounted: function() {
this.$root.$el.append(this.$el);
},
destroyed: function() {
this.$el.parentNode.removeChild(this.$el);
}
}
</script>
On mounted the element is moved inside of where the top level VUE app instance is mounted.
On destroyed removes the placeholder DOM comment for the migrated component from the new parent to prevent orphaned duplication each time the component remounts it's self.
VUE officially states not to destroy an element outside of VUE so this is not to be confused with that statement, here the component has already been destroyed.
This DOM comment duplication will typically happen when for example switching views with vue-router as this mechanism mounts and dismounts all components in a router view each time vue-router view state changes.
This behaviour is a bug cause by vue-router, the object is destroyed properly by VUE render manager but an index reference remains by mistake, using a portal package resolves this issue.
Here is the result:

Why does my Vue component require :key?

I have a small Vue.js component which displays a favorite star icon. Clicking on the icon favorites/unfavorites the element. So far I have only implemented the UI part, which looks like this:
<template>
<div :key="favorite">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="favorite" class="text-warning fas fa-star"></i>
<i v-show="!favorite" class="text-warning far fa-star"></i>
</a>
</div>
</template>
<script>
export default {
data() {
return {
favorite: true,
}
},
mounted() {
},
methods: {
toggleFavorite() {
this.favorite = !this.favorite
}
},
props: ['team-id'],
}
</script>
<style scoped>
</style>
As you can see, the logic is pretty simple.
This works well, but one thing that bothers me is that, if I remove the :key property from my template, the icon is not updated when I click on it (even though I have checked that the underlying property is indeed updated correctly). Adding :key makes it work, I imagine because it forces Vue.js to completely re-render the component when favorite is updated.
Why is this happening? I'm fairly new to the world of JS frameworks, so forgive any obvious stuff I might be missing. I did some research online but couldn't find an explanation. I just want to make sure I'm doing things the right way and not merely hacking around the issue here.
Vue patches with the virtual DOM whenever it is necessary. That is, whenever vue detects the changes on the DOM, it patches them for faster performance. And patching in the DOM will not change the icon or image. You need to replace the DOM instead.
Thus, vue provides the way for us whenever we need to change the DOM by replacing method, we can use :key binding.
So, :key binding can be used to force replacement of an element/component instead of reusing it.
The following whole html div will be replaced whenever there is change in favorite data as we're :key binding on it:
<div :key="favorite">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="favorite" class="text-warning fas fa-star"></i>
<i v-show="!favorite" class="text-warning far fa-star"></i>
</a>
</div>
This is why vue forcefully allows us to use :key binding inside a loop as there's need of replacing the elements inside the loop whenever it detects the changes in the data. This is made compulsory from 2.2.0+ and ESLint also have implemented this feature so that if you miss :key binding inside the loop, then you'll see the error on that line when you use editor that supports eslint, so that you can fix the error.
Just an opinion, the strict requirement of the :key binding should be removed from the vue as we might want a loop of predefined data and don't want to change the DOM but we still use the v-for loop for listing bigger data. But it might be rare case though.
Read carefully on the documentation for :key binding and then you'll have an idea.
The :key binding can be useful when you want to:
Properly trigger lifecycle hooks of a component
Trigger transitions
Use :key binding to replace the DOM. Remember it slower the performance as it replace the whole DOM that is bound to the element.
Don't use :key binding when you don't want to replace the DOM or
you think there's no data changes detection required. This will
allow vue to perform better without :key binding.
Its seems to be a general issue of FontAwesome CSS regardless the framework.
There is an issue on github and here the same issue with react https://github.com/FortAwesome/Font-Awesome/issues/11967
To prove that, here is a simplified version of the same example but using bootstrap icons
new Vue({
el: '#app',
data() {
return {
fav: true
}
}
});
<script
src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"
></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<div id="app">
<div>
<a v-on:click="fav = !fav" style="cursor: pointer">
<i v-show="fav" class="glyphicon glyphicon-star"></i>
<i v-show="!fav" class="glyphicon glyphicon-star-empty"></i>
</a>
</div>
</div>
You shouldn't need the :key, it's only necessary in v-for loops. I would suggest you remove it and replace your v-show with a v-if and v-else directive.
<i v-if="favorite" class="text-warning fas fa-star"></i>
<i v-else class="text-warning far fa-star"></i>
v-if removes and addes the section to the DOM whereas v-show just hides it so this way well resolve your issue
Ok I think the problem here is that you're changing your root data object. To preserve reactivity, you shouldn't change the root data object after you've instantiated Vue.
Here is your code in a simple Vue. I didn't need :key to make it work. I would keep :key for inside loops.
markup
<div id="vueRoot">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="store.favorite" class="text-warning fas fa-star">Fav</i>
<i v-show="!store.favorite" class="text-warning far fa-star">Not fav</i>
</a>
</div>
code
vm = new Vue({
el : "#vueRoot",
data() {
return { store :{
favorite: true
}}
},
mounted() {
},
methods: {
toggleFavorite() {
this.store.favorite = !this.store.favorite
}
}
}
);
This is a working example with minimal changes. From what you've showed us, you should just have <i> element, then do what you want with a dynamic class list, like...
<i :class="['text-warning','fa-star',store.favorite?'fas':'far']"></i>