Vue2 - how to recreate component? - vuejs2

How can I recreate a whole component after button click.
Let say I'm in Component "UserPanel" and there is a button call "Refresh".
When I click that button I would like to destroy component and create it from the scratch. I don't wont to use option like "vm.$forceUpdate()" because it doesn't help in my case.
Is it any way to do it?
My app code:
App.vue:
<template>
<div id="main-cont">
<NavBar></NavBar>
<router-view></router-view>
</div>
</template>
<script>
import NavBar from './components/TopBar/NavBar';
import {mapActions,mapGetters} from 'vuex';
import axios from 'axios';
export default {
name: 'App',
components: {
NavBar,
},
computed:{
...mapGetters(['isLoggedIn'])
},
methods:{
...mapActions(['loadLanguage','setToken','setUserLogged','loadUserProfile'])
},
created(){
this.loadLanguage();
this.setToken();
let userLoggedIn = document.head.querySelector('meta[name="logged"]').content;
if(userLoggedIn){
this.setUserLogged();
this.loadUserProfile();
}
}
}
</script>
<style scoped>
#main-cont{
height: 100%;
}
</style>
main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueCookie from 'vue-cookie';
import store from './store';
import App from './App';
//Components
import Main from './components/main/Main';
import UserRegister from './components/user/UserRegister';
import ResetPassword from './components/user/ResetPassword';
import UserEdit from './components/user/UserEdit';
import UserView from './components/user/UserView.vue';
import GameMain from './components/game/GameMain';
import GamesList from './components/main/GameList';
import Hall from './components/main/Hall';
import Language from './components/main/Language';
import GameCreate from './components/game/GameCreate';
//Plugins
import langPlugin from './langPlugin';
import VTooltip from 'v-tooltip';
Vue.use(VueRouter);
Vue.use(VueCookie);
Vue.use(langPlugin);
export const router = new VueRouter({
mode: 'history',
routes: [
{path: '/', component: Main},
{path: '/user-register', component: UserRegister},
{path: '/user-edit', component: UserEdit},
{path: '/password-reset', component: ResetPassword},
{path: '/user', component: UserView},
{path: '/game', component: GameMain},
{path: '/game-create', component: GameCreate},
{path: '/games-list', component: GamesList},
{path: '/hall-of-fame', component: Hall},
{path: '/language', component: Language},
]
});
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
Component to Reload.
GameCreate:
<Template>
<div>
<button #click="reloadThisComponent"></button>
</div>
</Template>
<script>
export default{
name: 'GameCreate',
methods:{
reloadThisComponent(){
}
}
}
</script>
Thank you.

EDIT (with the new question details) : Since you're using view router and your component is registred as a route, juste simply add the following to your rebuild method in your Game component this should works fine
this.$router.go(this.$router.currentRoute)
const router = new VueRouter({
mode: 'history',
})
new Vue({
router,
el: '#app',
methods: {
reload: function() {
this.$router.go(this.$router.currentRoute)
}
},
created() {
console.log("Hey")
}
})
<script src="https://npmcdn.com/vue/dist/vue.js"></script>
<script src="https://npmcdn.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<button #click="reload">Reload</button>
<router-view></router-view>
</div>
The simple way to get to the goal is to set a boolean in v-if on your component. Then switch true/false the boolean. When v-if is false the component is destroyed and will be reinstanciate after.
To do this, there is two way. What we want is to change de state of the parent component that will say if we print our component or not. The first way to do it is by using a State Management plugin like VueX, but it's a little bit too much for what we simply want to do. To be simpliest, we have to trigger an event from your component, that will trigger the state change on the parent.
In the exemple bellow, when you click on the reset button inside MyComponent,custom event named "reset" is emitted. In the parent component, we have a showMyComponent boolean on our MyComponent tag and a listener #reset that will trigger the method named "resetMyComponent" when the event "reset" is emmited by our MyComponent.
Here is a few resources :
What is VueX and State Management if you want know more about the first way to do it
Documentation about Custom Events in VueJS
Doc about Reactivity in Deep, not important here but it's for the explaination of this.nexttick usage here
Hope it's more clear now
var MyComponent = Vue.component('my-component', {
name : "my-component",
template : "#my-component-template",
data(){
return {
interval : null,
count : 0
}
},
created() {
console.log("MyComponent is created")
this.interval = setInterval(() => {
this.count++
},1000)
},
destroyed() {
console.log("MyComponent is destroyed")
clearInterval(this.interval)
}
});
new Vue({
el: "#app",
components : {
MyComponent
},
data: {
showMyComponent : true
},
methods : {
resetMyComponent() {
this.showMyComponent = false;
Vue.nextTick(() => {
this.showMyComponent = true;
});
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<my-component v-if="showMyComponent" #reset="resetMyComponent"></my-component>
</div>
<script type="text/x-template" id="my-component-template">
<div style="padding: 15px">
<p>My component is created since {{count}} seconds</p>
<button #click="$emit('reset')">Reset my component</button>
</div>
</script>

There are multiple ways of recreating components. The most efficient way is to change the component key. What we do here is we will supply a key attribute so Vue knows that a specific component is coupled or tied to a specific piece of data. If the key stays the same, it won't change the component, but if the key changes, Vue knows that it should get rid of the old component and re-create a new one.
Here is a very basic way of doing it:
<template>
<component-to-re-render :key="componentKey" />
</template>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
Every time that forceRerender is called, our prop componentKey will change. When this happens, Vue will know that it has to destroy the component and create a new one. What you get is a child component that will re-initialize itself and “reset” its state. this simple and elegant way is solving the most common challenge we face in the Vue app development!
You can also check other possible ways od doing this: https://medium.com/emblatech/ways-to-force-vue-to-re-render-a-component-df866fbacf47

Related

I can not move another path in Vue JS

Im new to Vue JS and I'm making a simple page in Vue JS. Here are my codes:
main.js
import Vue from 'vue'
import App from './App.vue'
import PokeProfile from './components/PokeProfile.vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import VueRouter from 'vue-router';
Vue.use(VueRouter)
Vue.use(ElementUI)
const router = new VueRouter({
routes: [
{path: '/', component: App},
{path: '/pokemon/:id', component: PokeProfile},
],
mode: 'history'
})
//Vue.config.productionTip = false
new Vue({
el: '#app',
render: h => h(App),
router: router
})
App.js
<template>
<div id="app">
<div class="tag-group">
<el-tag
v-for="pokemon in pokemons"
:key="pokemon.national_id"
:type="pokemon.name"
effect="plain">
<poke-item :pokemon=pokemon></poke-item>
</el-tag>
</div>
</div>
</template>
<script>
import PokeItem from './components/PokeItem.vue'
import axios from 'axios'
export default {
name: 'app',
components: {
PokeItem
},
data() {
return {
pokemons: []
}
},
created() {
axios.get("http://localhost:3000")
.then(res => this.pokemons = res.data)
.catch(err => {console.log(err)})
}
}
</script>
<style>
div {
display: flex;
justify-content: center;
}
</style>
PokeItem.js
<template>
<div>
<router-link :to="pokemonLink">
{{pokemon.name}}
</router-link>
</div>
</template>
<script>
export default {
data() {
return {}
},
props: {
pokemon: {
type: Object,
required: true
}
},
computed: {
pokemonLink() {
return `/pokemon/${this.pokemon.national_id}`
}
}
}
</script>
PokeProfile.js
<template>
<h1>Hello Pokemon</h1>
</template>
<script>
export default {
}
</script>
The problem here is I can not move to PokeProfile.js when I click on an item in the PokeItem.js file. What could be the problem? I've checked the section of the code related to routing but I didn't see any problem.
Vue-Router uses a dynamic component (<router-view>) to render the components of your routes. Usually you will find this component in the template of your app.vue. Since you have no <router-view> component Vue-Router does not know where to render your route components.
Try this:
// main.js
import Home from './components/Home.vue'
const router = new VueRouter({
routes: [
{path: '/', component: Home},
{path: '/pokemon/:id', component: PokeProfile},
],
mode: 'history'
})
// components/Home.vue
// your old App.vue
// ./App.vue
<template>
<main>
<router-view></router-view>
</main>
</template>

Setting up websocket listener inside of Vue.js component

I've created a component which will be used multiple times on the site and represents a row of a table.
Now I want each instance of this component to update itself according to the messages received via websocket.
I tried to register an onmessage listener in the created() method of the component, but it doesn't seem to work like this. created() gets called for each row of the table, but the listener doesn't show any effect.
When I register the listener in my "main" App.vue it's working. But I'd like it in my component so I have easy access to it's properties.
Can you give me some hints what I'm doing wrong?
Here's a sample of my component:
<template>
<div class="feed-item">
<div class="feed-element">{{feed.name}}</div>
<div class="feed-element">{{feed.idle}}</div>
<div class="feed-element">{{feed.unseen}}</div>
<div class="feed-element">{{feed.last_message}}</div>
<div class="feed-element">{{feed.page}}</div>
</div>
</template>
<style scoped>
/* ... */
</style>
<script>
export default {
name: 'feedItem',
props: {feed: Object},
created () {
this.$options.sockets.onmessage = (data) => console.log(data)
},
}
</script>
And I loaded vue-native-websocket im my main.js:
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import VueNativeSock from 'vue-native-websocket'
Vue.use(VueNativeSock, 'ws://127.0.0.1:9000', {
format: 'json',
// store: store,
reconnection: true,
reconnectionDelay: 1000
})
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App },
})

How to sync states between backend and frontend using vuex and vue-router?

I am developing a single-page-application using vue-cli3 and npm.
The problem: Populating a basic integer value (stored in a vuex state) named counter which was incremented/decremented in the backend to the frontend, which displays the new value.
The increment/decrement mutations are working fine on both components (Frontend/Backend), but it seems like the mutations don't work on the same route instance: When incrementing/ decrementing the counter in backend, the value is not updated in the frontend and otherwise.
store.js:
Contains the state which needs to be synced between Backend/Frontend.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 10
},
mutations: {
increment (state) {
state.counter++
},
decrement (state) {
state.counter--
}
}
})
index.js:
Defines the routes that the vue-router has to provide.
import Vue from 'vue'
import Router from 'vue-router'
import Frontend from '#/components/Frontend'
import Backend from '#/components/Backend'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Frontend',
component: Frontend
},
{
path: '/backend',
name: 'Backend',
component: Backend
}
],
mode: 'history'
})
main.js:
Inits the Vue instance and provides the global store and router instances.
import Vue from 'vue'
import App from './App'
import router from './router'
import { sync } from 'vuex-router-sync'
import store from './store/store'
Vue.config.productionTip = false
sync(store, router)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Frontend.vue/Backend.vue:
Both (Frontend/Backend) use the same code here.
They use the state counter in order to display and modify it.
<template>
<div> Counter: {{ getCounter }}
<br>
<p>
<button #click="increment">+</button>
<button #click="decrement">-</button>
</p>
</div>
</template>
<script>
export default {
name: 'Frontend',
methods: {
increment () {
this.$store.commit('increment')
},
decrement () {
this.$store.commit('decrement')
}
},
computed: {
getCounter () {
return this.$store.state.counter
}
}
}
</script>
It would be awesome if someone sould tell me what I am missing or if I have misunderstood the concept of vuex and vue-router.
Just get the counter from the store for both components. You don't need data as store is already reactive.
<template>
<div> Counter: {{ counter }}
<br>
<p>
<button #click="increment">+</button>
<button #click="decrement">-</button>
</p>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'Frontend',
methods: {
...mapMutations([
'increment',
'decrement',
])
},
computed: {
...mapState({
counter: state => state.counter,
})
}
}
</script>
For reference:
mapState: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper
mapMutations: https://vuex.vuejs.org/guide/mutations.html#committing-mutations-in-components
#sebikolon component properties that are defined in data () => {} are reactive, methods are not, they are called once. Instead of {{ getCounter }}, just use {{ $store.state.counter }}. OR initiate property in each component that gets the value of your state.
data: function () {
return {
counter: $store.state.counter,
}
}

Why is the activated lifecycle hook not called on first visit

I have a problem where a component within a router-view that is being kept alive does not call its activated lifecycle hook when first created. The created and mounted lifecycle hooks are being called. On a second visit, the activated hook is being called.
The scenario is quite complicated as there is a bit of nesting and slot using involved.
I've tried to create a minimal example which you can find below, or a bit more detailed on https://codesandbox.io/s/251k1pq9n.
Unfortunately, it is quite large and still not as complicated as the real code which I unfortunately cannot share.
Worse, I failed to reproduce the actual problem in my minimal example. Here, the created, mounted, and activated lifecycle hooks are all called when first visiting SlotExample.
In my real code, only the created and mounted, lifecycle hooks are called on the first visit, the activated hook is called on subsequent visits. Interestingly, all lifecycle hooks are called as expected for SlotParent.
The real code involves more nesting and makes use of slots to use layout components.
My code is using Vue 2.5.16 and Vue-Router 3.0.1 but it also doesn't work as expected in Due 2.6.7 and Vue-Router 3.0.2. I am also using Vuetify and Vue-Head but don't think think this has anything to do with my problem.
index.js.
Does anyone have an idea what I could have been doing wrong. I actually suspect a bug in vue-router
when using multiple nested slots and keep-alive but cannot reproduce.
index.js
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import Start from "./Start.vue";
import SlotExample from "./SlotExample.vue";
const routes = [
{
path: "/start",
component: Start
},
{
path: "/slotExample/:id",
component: SlotExample,
props: true
}
];
const router = new VueRouter({ routes });
Vue.use(VueRouter);
new Vue({
render: h => h(App),
router,
components: { App }
}).$mount("#app");
App.vue
<template>
<div id="app">
<div>
<keep-alive><router-view/></keep-alive>
</div>
</div>
</template>
SlotExample.vue
<template>
<div>
<h1>Slot Example</h1>
<router-link to="/start"><a>start</a></router-link>
<router-link to="/slotExample/123">
<a>slotExample 123</a>
</router-link>
<slot-parent :id="id">
<slot-child
slot-scope="user"
:firstName="user.firstName"
:lastName="user.lastName"/>
</slot-parent>
</div>
</template>
<script>
import SlotParent from "./SlotParent.vue";
import SlotChild from "./SlotChild.vue";
export default {
name: "slotExample",
components: { SlotParent, SlotChild },
props: {
id: {
type: String,
required: true
}
}
};
</script>
SlotParent.vue
<template>
<div>
<div slot="header"><h1>SlotParent</h1></div>
<div slot="content-area">
<slot :firstName="firstName" :lastName="lastName" />
</div>
</div>
</template>
<script>
export default {
name: "slotParent",
props: {
id: {
type: String,
required: true
}
},
computed: {
firstName() {
if (this.id === "123") {
return "John";
} else {
return "Jane";
}
},
lastName() {
return "Doe";
}
}
};
</script>
SlotChild.vue
<template>
<div>
<h2>SlotChild</h2>
<p>{{ firstName }} {{ lastName }}</p>
</div>
</template>
<script>
export default {
name: "slotChild",
props: {
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
}
},
created() {
console.log("slotChild created");
},
mounted() {
console.log("slotChild mounted");
},
activated() {
console.log("slotChild activated");
}
};
</script>
I think you need to put SlotChild within keep-alive block.
Take a look at vue js doc about activated hook

Using vue-router programmatically

I'm having issues when trying to use vue-router programatically.
When I use <router-link> in my HTML it works no problem but as soon as I try to use this.$router.push I have no luck. Below is a minimum viable snippet.
main.js
import Vue from 'vue';
import App from './App';
import router from './router';
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});
router index.js
import Vue from 'vue';
import Router from 'vue-router';
// Pages
import Home from '#/components/pages/Home';
import CalculatorSuite from '#/components/pages/calculator-suite/CalculatorSuite';
Vue.use(Router);
export default new Router({
routes: [
{ path: '/', name: 'Home', component: Home },
{ path: '/calculator-suite', component: CalculatorSuite},
],
});
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
Home.Vue
<template>
<div class="home-parent">
// Works
<router-link to="/calculator-suite">Test</router-link>
//Doesn't work
<button :onClick="change">Test</button>
</div>
</template>
<script>
export default {
name: 'Home',
methods: {
change() {
// Do stuff before changing
this.$router.push('/calculator-suite');
},
},
};
</script>
What can I do to allow this to work?
You have a problem on the listener of "click" event. You are biding a value when you should be doing #click instead of :onClick to listen to the click event.
More info about binding: https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage
More info about event handling: https://v2.vuejs.org/v2/guide/events.html
Instead of onclick you need to replace it with #click or v-on:click like as below
<button #click="change">Test</button> // shorthand
or
<button v-on:click="change">Test</button>
You can also use href tag to go to desired page.like as below
<button>Test</button>