Vue single file component import lifecycle hooks - vue.js

I need help with the setup of my Vue application (I am just learning vue).
I read in the tutorials that to get access to a lifecycle hook I would need to do something like this:
<template>
<h4>Sports</h4>
<li v-for="sport in sports" v-bind:key="sport.id">
{{ sport.name }}
</li>
</template>
<script lang="ts">
import { onMounted } from "vue";
export default {
setup() {
onMounted(() => {
console.log("component mounted");
});
},
data() {
return {
sports: [],
};
},
};
</script>
However, VSCode's intellisense doesn't recognize onMounted as an exported function from vue. When I run my code in snowpack it still doesn't recognize the function.

I think the issue is likely due to lang="ts"
You could try having the js in a separate file and referencing it with <script lang="ts" src="./myComponent.ts"> and then have the typescript in there.
Here is some documentation someone came up with regarding, what seems to be, the same/related issue.
https://github.com/patarapolw/vue-typescript-suggestions

You don't need to import them, they are already available:
<template>
<h4>Sports</h4>
<li v-for="sport in sports" v-bind:key="sport.id">
{{ sport.name }}
</li>
</template>
<script>
export default {
data() {
return {
sports: [],
};
},
mounted() {
console.log("component mounted");
};
};
</script>
Also, unless you specifically want to use typescript, then leave off lang="ts" in the script tag.

Related

Async loading child component doesn't trigger v-if

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

vm.$on listener isn't firing from the parent mounted() method

It seems very simple but not working for me. I am trying to fire event from a child component and listen to it from the parent component using the mounted() method by using the vm.$on() instance method but it's not working.
For example, I have created a very basic Vue CLI App on CodeSandbox to reproduce the issue. Any help would be much appreciated.
Parent component: App.vue
<template>
<div id="app">
<HelloWorld />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld
},
mounted() {
this.$on('icecream', () => console.log('not good for children'));
},
};
</script>
Child component: HelloWorld.vue
<template>
<div class="hello">
<h3>Emit event from child and listen from parent</h3>
<button #click="emitAnEvent()">Emit</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
methods: {
emitAnEvent() {
this.$emit('icecream');
}
},
};
</script>
There's also another option to handle the $emit() written in the Vue Documentation here.
<HelloWorld v-on:icecream="CatchIceCream()" />
.
.
.
methods: {
CatchIceCream() {
console.log('not good for children');
}
},
Never mind! I was able to figure it out based on the answer given here. I had to $emit() event directly to the $parent like the following:
this.$parent.$emit('icecream');
While this could be a bit problematic in different scenarios, it's completely fine on my case.

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

Pass data from one component to all with $emit without using #click in VueJS

Trying to learn vuejs I got to the question how to pass any data from one component to all, using $emit but without using any #click.
It is possible some how that the data to be just available and grab it any time, without using the click?
Let's say we have this example with normal #click and $emit.
main.js
export const eventBus = new Vue()
Hello.vue
<template>
<div>
<h2>This is Hello component</h2>
<button
#click="emitGlobalClickEvent()">Click me</button>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
methods: {
emitGlobalClickEvent () {
eventBus.$emit('messageSelected', this.msg)
}
}
}
</script>
User.vue
<template>
<div>
<h2>This is User component</h2>
<user-one></user-one>
</div>
</template>
<script>
import { eventBus } from '../main'
import UserOne from './UserOne.vue'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log(msg)
})
},
components: {
UserOne
}
}
</script>
UserOne.vue
<template>
<div>
<h3>We are in UserOne component</h3>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log('From UserOne message !!!')
})
}
}
</script>
I want to get this message : Welcome to Your Vue.js App from Hello.vue in all components, but without #click, if is possible.
You can create another Javascript file which holds an Object with your initial state. Similar to how you define data in your components.
In this file your export your Object and import it in all Components which need access to this shared state. Something along the lines of this:
import Store from 'store';
data() {
return {
store
}
}
This might help:
https://v2.vuejs.org/v2/guide/state-management.html
At this point if you app grows even more in complexity you might also start checking out Vuex which helps to keep track of changes(mutations) inside of your store.
The given example is essential a very oversimplified version of Vuex.