Creating multiple instances of a component without multiple imports - javascript

So currently I'm importing a component multiple times with different names.
import Page1 from "./Page/Page"
import Page2 from "./Page/Page"
import Page3 from "./Page/Page"
import Page4 from "./Page/Page"
I'm doing this as I want each instance to have its own set of properties, which then I use <keep-alive> to maintain their state.
I am also using them inside a <component :is="".
I was wondering if there was a way to create multiple instances without multiple import.
Codesandbox:
https://codesandbox.io/s/5x391j8y4x
you will notice that if I switch between the HelloWorlds, that the input will maintain their instances (input will change to what they were holding)

You don't need to use <component> because you only have one component type that you want to use: HelloWorld. <component> is only needed when you want to dynamically render different component types.
The reason why you require <keep-alive> is because the HelloWorld component has local state (msg) which will be lost once the component instance is destroyed.
You will need to use key to force Vue to instantiate a new instance of HelloWorld based on the page, and you need <keep-alive> to prevent each instance from being destroyed when you switch between pages.
Here's an example:
<ul>
<li
v-for="page in pages"
#click="currentPage = page"
:key="page.key">{{ page.title }}</li>
</ul>
<keep-alive>
<hello-world
:key="currentPage.key"
:title="currentPage.title"/>
</keep-alive>
import HelloWorld from './components/HelloWorld'
export default {
name: "App",
components: {
HelloWorld,
},
data() {
const pages = [
{
key: "home",
title: "Home"
},
{
key: "about",
title: "About"
},
{
key: "contact",
title: "Contact"
}
]
const currentPage = pages[0]
return {
currentPage,
pages
}
}
}

Related

Toggle classes in different components in Nuxt.js

Let's say I created a plugin in my Nuxt app:
plugins: [
'~/plugins/toggler',
],
which contains:
import Vue from 'vue'
Vue.mixin({
data() {
return {
isActive: false
}
},
methods: {
toggler() {
this.isActive = !this.isActive
},
},
})
I would like to toggle classes on elements on different components, like:
<div :class="{ 'is-active': isActive }"></div>
and it's correctly triggered via a button:
<button #click.prevent="toggler" />
However, this only change the class on other elements inside the same .vue component where the button is.
Other elements with the same :class="{ 'is-active': isActive }" in different .vue components aren't affected, unless I add a button in those components too.
Here's a sandbox: https://codesandbox.io/s/tender-http-fqlx5
Why you create a mixin in a plugin?
Create and usage mixin:
create:
In the mixins folder inside You're creating a mixin file (example.js)
usage:
import into the component:
import example from '~/mixins/example'
Mixin definition into component into script:
export default {
mixins: [example],
data() {
return {}
},
computed: {},
methods: {}
}
This allows it to be reused into any component.

Vue - Pass components to router-view on dynamically loaded component

I need to render a different layout for the same route for a specific URI with different components depending on the user being on mobile or in desktop.
I would like to avoid having route path checks in the PageCommon(layout component) to keep it clean.
The app has a main component taking care of the layout, it has different router-views where we load the different components for each page URI. This would be a normal route for that.
{
path: '',
component: PageCommon,
children: [
{
path: '',
name: 'Home',
components: {
default: Home,
header: Header,
'main-menu': MainMenu,
'page-content': PageContent,
footer: Footer,
'content-footer': ContentFooter
}
},
I can't change the route components property once the component is loaded so I tried to make a wrapper and pass the components dynamically.
{
path: 'my-view',
name: 'My_View',
component: () => import('#/components/MyView/ViewWrapper')
},
In /components/MyView/ViewWrapper'
<page-common v-if="isMobile">
<my-mobile-view is="default"></my-mobile-view>
<main-menu is="main-menu"></main-menu>
</page-common>
<page-common v-else>
<my-desktop-view is="default"></my-desktop-view>
<header is="header"></header>
<main-menu is="main-menu"></main-menu>
<footer is="footer"></footer>
</page-common>
</template>
I would expect that the components passed inside page-common block would be substituted on the appropriate , but is not how it works, and Vue just loads page-common component with empty router-views.
Is there any approach for this?
Note that I already tried using :is property for loading different components, but the problem then is on how to tell the parent to use this or that component for this page. This is the code for that:
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
data(){
return {
myView: null,
isMobile: this.detectMobile()
}
},
methods : {
getViewComponent() {
return this.isMobile ? 'mobile-view' : 'desktop-view';
}
},
created() {
this.myView = this.getViewComponent();
}
}
</script>
I could use this approach for each of the PageCommon router views, creating a component for each that does the above, but it looks like a very bad solution.
A computed method is all you need.
You should have this top level Logic in App.vue and the <router-view> should be placed in both DesktopView and MobileView.
// App.vue
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
computed: {
myView() {
return this.detectMobile() ? 'mobile-view' : 'desktop-view';
}
}
}
</script>
You may also want to consider code splitting by setting up Dynamic Components for those layouts since Mobile will load Desktop View because it is compiled into final build, register them globally as dynamic imports instead if importing them in MyView and then delete components also after doing the following instead, this way only the one that is needed will be downloaded saving mobile users their bandwidth:
// main.js
import LoadingDesktopComponent from '#/components/LoadingDesktopComponent '
Vue.componenet('desktop-view', () => ({
component: import('#/components/MyView/DesktopView'),
loading: LoadingDesktopComponent // Displayed while loading the Desktop View
})
// LoadingDesktopComponent .vue
<template>
<div>
Optimizing for Desktop, Please wait.
</div>
</template>
<script>
export default {
name: 'loading-component'
}
</script>
Routing logic will only be processed when <router-view> is available,this means you can delay the presentation of Vue Router, for example you can have :is show a splash screen like a loading screen on any URI before displaying a component in :is that contains <router-view>, only than at that point will the URI be processed to display the relevant content.

How to import dynamic v-bind:is component with module system in Vue

Problem: I am trying to programmatically register a component to be used in a slot in my Vue/Nuxt site. The component name is included in the data of the parent index.vue file, in this instance the component is named Projects. I am including it in a v-for template as the various objects in the 'modules' data array are iterated over. I had assumed this would be possible/easy from the dynamic component documentation and example however I have not managed to get it working in my case.
What I expect to happen: I expected the component to be registered and 'slotted' into the Module component correctly.
What actually happens: While I can see on the rendered view that the component is 'there', it is not in the correct place (i.e. slotted into the Module component). I also get a vue/no-unused-components error saying: The "Projects" component has been registered but not used.
I have read the documentation about component registration in modular systems but these seem to be for more complex cases than what I am trying to achieve. Any advice would be really helpful as I am totally stuck!
index.vue
<template>
<div>
<template v-for="module in modules">
<Module
:title="module.title"
:to="module.link"
/>
<component v-bind:is="module.slot" />
</Module>
</template>
</div>
</template>
<script>
import Module from '~/components/module/Module.vue'
import Projects from '~/components/module/slots/Projects.vue'
export default {
components: {
Module,
Projects
},
data () {
return {
modules: [
{
title: 'Work',
slot: 'Projects'
},
{
...
}
]
}
}
}
</script>
Edit: As a side note, I get the same error when registering the component with import like so:
components: {
Module,
'Projects': () => import('#/components/module/slots/Projects')
}
Module.vue
<template>
<div>
<h2>
{{ title }}
</h2>
<slot />
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
}
}
}
</script>
Projects.vue
<template>
<div>
<h3>Projects</h3>
</div>
</template>
<script>
export default {
name: 'Projects'
}
</script>
You use the self closure tag in your Module component.
This prevents your Projects component to be rendered within the slot.
Just replace:
<Module
:title="module.title"
:to="module.link"
/>
with:
<Module
:title="module.title"
:to="module.link"
>
and it should work.

Multiple components, but only first one rendered

I'm trying to create simple ToDo app using Ractive.js and Redux, but I ran into a problem with rendering more than one component on my page.
Javascript code:
import Ractive from 'ractive';
import template from './Home.html';
import './Home.less';
import { AddToDoForm, ToDoList } from '../';
export default Ractive.extend({
template,
data: {
message: 'This is the home page.',
},
components: {
AddToDoForm,
ToDoList
}
});
HTML of the component:
<AddToDoForm store={{store}} />
<ToDoList store={{store}} />
But only the first component is rendered. The store parameter I'm passing is the Redux store, but it doesn't work even without it.
I would add to verify defaults as a
...
components:{
AddToDoForm: AddToDoForm,
ToDoList: ToDoList
}
...
syntax examples (answer/31096635)

VueJS 2: Catch event of direct child component

I'm currently trying to get a simple Tabs/Tab component up and running.
It seems like something in the event handling mechanism has changed, therefore I can't get it to work.
Current implementation:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<li class="tab" v-for="tab in tabs" #click="activateTab(tab)">{{ tab.header }}</li>
</ul>
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: [],
data() {
return {
tabs: []
}
},
created() {
this.$on('tabcreated', this.registerTab)
},
methods: {
registerTab(tab) {
this.tabs.push(tab);
},
activateTab(tab) {
}
}
}
</script>
Tab.vue
<template>
<div class="tab-pane" v-show="active">
<slot></slot>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
mounted() {
this.$emit('tabcreated', this);
}
}
</script>
eventhub.js
import Vue from 'vue';
export default new Vue();
View
<tabs>
<tab header="Test">
First Tab
</tab>
<tab header="Test2">
Second Tab
</tab>
<tab header="Test3">
Third Tab
</tab>
</tabs>
I've tried the following things:
use a Timeout for the $emit to test if it's a timing issue (it is
not)
use #tabcreated in the root element of the Tabs components
template
It works if...
... I use the suggested "eventhub" feature (replacing this.$on and
this.$emit with hub.$on and hub.$emit)
but this is not suitable for me, as I want to use the Tabs component multiple times on the same page, and doing it with the "eventhub" feature wouldn't allow that.
... I use this.$parent.$emit
but this just feels weird and wrong.
The documentation states that it IS possible to listen for events triggered by $emit on direct child components
https://v2.vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced
Does anyone have an Idea?
You're right, in vue 2, there is no more $dispatch. $emit could work for a single component but it will be scoped to himself (this). The recommended solution is to use a global event manager, the eventhub.
the eventhub can be stored in the window object to be used anywhere without import, I like to declare in my main.js file like this:
window.bus = new Vue()
and then in whatever component:
bus.$emit(...)
bus.$on(...)
It works just the same as this.$root.$emit / this.$root.$on. You said it works when you call this.$parent.$emit, but this code, simulate a scoped emit in the parent component but fired from the child, not good.
What I understand in your code is that you want to have an array of created tabs, but to do what with them ?
Instead of storing the tab instance in the parent and then activate from the parent, you should think about a more functional way.
The activateTab method should be declared on the tab component and manage the instanciation through the data, something like:
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tab in tabs" :header="tab.header"></tab>
</ul>
</div>
</template>
<script>
import hub from '../eventhub';
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: ['First Tab', 'Second Tab', 'Third Tab']
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ header }}</span>
</div>
</template>
<script>
import hub from '../eventhub';
export default {
props: {
'header': String
},
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This way, your Tab is more independant. For parent/child communication keep this in mind :
parent to child > via props
child to parent > via $emit (global bus)
If you need a more complexe state management you definitely should take a look at vuex.
Edit
Tabs.vue
<template>
<div class="tabbed-pane">
<ul class="tab-list">
<tab v-for="tabData in tabs" :custom="tabData"></tab>
</ul>
</div>
</template>
<script>
import Tab from 'path/to/Tab.vue';
export default {
components: [Tab],
props: [],
data() {
return {
tabs: [
{foo: "foo 1"},
{foo: "foo 2"}
{foo: "foo 3"}
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab tab-pane" #click:activeTab()>
<span v-show="active">Activated</span>
<span>{{ custom.foo }}</span>
</div>
</template>
<script>
export default {
props: ['custom'],
data() {
return {
active: false
}
},
methods: {
activeTab () {
this.active = true
}
}
}
</script>
This is what I don't like about VueJS (2), there is no convenient way of catching events emitted from child components to the parent component.
Anyways an alternative to this is if you do not want to use the eventhub approach, specially if you are only going to have an event communication between related components ( child and parent ) and not with non-related components, then you can do these steps.
reference your parent vue component on its data property (very important, you can't just pass this to the child component)
pass that parent vue component reference as an attribute to the child component ( make sure to bind it)
trigger the appropriate event of the parent component inside the child component whenever a desired event is emitted
Pseudo code
// Parent vue component
Vue.component( 'parent_component' , {
// various codes here ...
data : {
parent_component_ref : this // reference to the parent component
},
methods : {
custom_event_cb : function() {
// custom method to execute when child component emits 'custom_event'
}
}
// various codes here ...
} );
// Parent component template
<div id="parent_component">
<child_component :parent_component_ref="parent_component_ref"></child_component>
</div>
// Child component
Vue.component( 'child_component' , {
// various codes here ...
props : [ 'parent_component_ref' ],
mounted : function() {
this.$on( 'custom_event' , this.parent_component_ref.custom_event_cb );
this.$emit( 'custom_event' );
},
// You can also, of course, emit the event on events inside the child component, ex. button click, etc..
} );
Hope this helps anyone.
Use v-on="$listeners", which is available since Vue v2.4.0. You can then subscribe to any event you want on the parent, see fiddle.
Credit to BogdanL from Vue Support # Discord.

Categories