How to pass props to a vue component at initialization inside single file vue components (dependency injection in vue-loader)? - vue.js

I'm building a TabbedDetailView reusable component in vue. The idea is that the tab-detail component receives a list of objects which have a title and a component. It then does the logic so that when you click on a tab, then the component is displayed. The problem is that this components have a prop that is a user_id. How do I insert this prop into the components from outside of the template (directly in the script)?
For example (using single file vue components with webpack):
TabDetail.vue
<template>
<div>
<nav class="tabs-nav">
<ul class="tabs-list">
<li class="tabs-item" v-for='tab in tabs'>
<a v-bind:class="{active: tab.isActive, disabled: !tab.enabled}" #click="switchTab(tab)">{{tab.title}}</a>
</li>
</ul>
</nav>
<div v-for='tab in tabs'>
<component :is="tab.detail" v-if='tab.isActive'></component>
</div>
</div>
</template>
<script>
export default {
name: 'NavigationTabs',
props: ['tabs'],
created: function() {
this.clearActive();
this.$set(this.tabs[0], 'isActive', true);
},
methods: {
clearActive: function() {
for (let tab of this.tabs) {
this.$set(tab, 'isActive', false);
}
}, switchTab: function(tab) {
if (tab.enabled) {
this.clearActive();
tab.isActive = true;
}
},
},
};
</script>
The idea is that this can be reused by only passing a props object with titles and components. eg. tabs = [{title: 'Example1', component: Component1}{title: 'Example2', component: Component2}] I want to be able to instantiate this components with props before passing them. eg. tabs = [{title: 'Example1', component: Component1({user_id: 5})}{title: 'Example2({user_id: 10})', component: Component2}]).
SomeComponent.vue
import Vue from 'vue';
import TabDetail from '#/components/TabDetail'
import Component1 from '#/components/Component1';
const Componenet1Constructor = Vue.extend(Component1);
export default {
data() {
return {
tabs: [
{title: 'Componenent 1', detail: new Component1Constructor({propsData: {user_id: this.user_id}})}
{title: 'Component 2', detail: Component2},
{title: 'Component 3', detail: Component3},
],
};
}, props: ['user_id'],
components: {'tab-detail': TabbedDetail},
}
<template>
<div>
<tab-detail :tabs='tabs'></tab-detail>
</div>
</template>
Component1.vue
export default {
props: ['user_id'],
};
<template>
<div>
{{ user_id }}
</div>
</template>
The approach above raises de error:
[Vue warn]: Failed to mount component: template or render function not defined.
I think this is a good idea because I'm trying to follow the dependency injection design pattern with components. Is there a better approach to this problem without using global state?

This is could be done via Inject Loader when using vue loader with single file vue components but it adds a lot of unnecessary complexity and it's mostly meant for testing. It seems like the preferred way of managing state is by using a global state management store like Vuex.

Related

2 Way Databind components within Components

I am struggling to reuse my components.
I want to pass the data passed to my component as a prop to another component.
If I do that vue complains about a mutation of the prop.
Example:
I have contacts that I want to show on multiple location of my app.
For that I created a contact component to reuse it:
<template>
<div>
<input :value="contact.firstName" #input="$emit('update:contact', {...contact, firstName: $event.target.value})">
<Mother v-model:mother="contact.mother"/>
</div>
</template>
<script>
import Mother from '#/components/Mother'
export default {
name: 'Contact',
components: {
Mother
},
props: {
contact: Object,
},
emit: ['update:contact'],
methods: {
}
}
</script>
Every contact has a mother, mother are shown in other places not only in the contact component.
That is why I created a mother component, that is used by the contact.
<template>
<div>
<input :value="mother.lastName" #input="$emit('update:mother', {...mother, lastName: $event.target.value})">
</div>
</template>
<script>
export default {
name: 'Mother',
props: {
mother: Object,
},
emit: ['update:mother'],
methods: {
}
}
</script>
Now I want to be able to mutate the contact an the mother as well, and I want to be able to use two contact components on the same site.
If I use it the way explained I get this error:
ERROR Failed to compile with 1 error 09:17:25
error in ./src/components/Contact.vue
Module Error (from ./node_modules/eslint-loader/index.js):
/tmp/vue-example/src/components/Contact.vue
4:27 error Unexpected mutation of "contact" prop vue/no-mutating-props
✖ 1 problem (1 error, 0 warnings)
I have an example project showing my problem:
https://gitlab.com/FirstWithThisName/vue-example.git
Thanks for your help
First I need to assume a few points.
You wanted to use v-model.
You wanted the component to be chained.
Working Example here on Vue SFC Playground.
*Note that the import path is different on the example site.
App.vue
<template>
<Contact v-model="contact" />
{{ contact }}
</template>
... remaining code omitted
Contact.vue
<template>
<div>
<input v-model="localValue"/>
<Mother v-model="childValue" />
</div>
</template>
<script>
import Mother from "./Mother.vue"
export default {
name: "Contact",
components: {
Mother
},
props: {
modelValue: Object,
},
mounted(){
this.childValue = this.modelValue.mother
},
data: () => ({
localValue: "",
childValue: null
}),
watch:{
updatedData(){
this.$emit('update:modelValue', this.updatedData)
}
},
computed: {
updatedData() {
return { firstName: this.localValue, mother: this.childValue };
},
},
};
</script>
Mother.vue
<template>
<div>
<input
v-model="localValue"
#input="$emit('update:modelValue', updatedData)"
/>
</div>
</template>
<script>
export default {
name: "Mother",
props: {
modelValue: Object,
},
data: () => ({
localValue: "",
}),
computed: {
updatedData() {
return { ...this.modelValue, lastName: this.localValue };
},
},
};
</script>
As you might know, props cannot be mutated, so you will need to "make a copy" of the value on each component to process locally.
If Mother component are never going to be used separately, v-model can be split into v-on and v-bind instead.
Lastly, as for recommendation, chaining like this can become very messy if the data starts to grow or the depth level increases. You could just make another Wrapper component that contains Contact and Mother component that scales horizontally instead.
Depends on how complex your application will get.
One option is two-way data-binding as explained here:
https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components
So you basically emit the changes to the parent.
For more complex applications I wouldn't pass data that are used in multiple components as props, but use a store. Either a simple reactive object; with provide/inject or use something like Vuex.

Vue props undefined on component

I am struggling with passing props to my child component and reading through many many examples, there are quiet a few in my position. Seems this shouldn't be so complicated, right?
Ideally, when I drop my component on a html page I want to be able to pass a url as an attribute. Example
<landingpage myUrl="http://localhost"><landingpage> but when I inspect with the Vue Dev Tools in browser, it is always undefined. I've seen a hack using JQuery to select the element and then get the attribute but I would like to do it in pure Vue.
In my code below, no variation of "title" is passed to my component.
In my index.html page I have this
<body>
<p>Hello world, this is some text. Howdy.</p>
<div id="NewWidget">
<div id="app" data-title="mario" :data-title="luigi" :title="princess">
<landingpage title="hello!" :title="spaghetti" v-bind:title="Nervos"></landingpage>
</div>
</div>
<!-- built files will be auto injected -->
</body>
In my App.vue I have
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
And in my landingpage.vue I have this
export default {
name: 'landingpage',
data () {
return {
categories: [],
}
},
props: {
title: {
type: String
}
},
...
My router index.js
export default new Router({
routes: [
{
path: '/',
name: 'LandingPage',
component: LandingPage,
props: true
},
...
In my LandingPage component, this.title is always null/undefined.
I am using Vue 2.5.2 / Vue Router 3.0.1
Only thing I can think of is my VueRouter usage in App.vue is burning the props?

Vue - v-model inside a component within a component

I'm trying to separate my project now into components to make the code readable when adjusting into a responsive app. The problem is passing the v-model from base-select -> child -> parent. How do I store the data selected to the Parent.vue items: ''? Here is my code below.
Parent.vue
<template>
<child></child>
</template>
<script>
import Child from './components/Child'
export default {
components: {
Child,
},
data: ()=> ({
item: ''
})
}
</script>
Child.vue
<template>
// Random HTML
// Random HTML 2
<base-select
:items="select"
>
</template>
<script>
import BaseSelect from '#/components/BaseSelect'
export default {
components: {
BaseSelect,
},
data: ()=> ({
select: ['Select 1', 'Select 2']
})
}
</script>
BaseSelect.vue
<template>
<v-select
v-bind="$attrs"
v-on="$listeners"
class="body-2"
solo
dense
clearable
/>
</template>
To implement v-model you need to add a value property to each child component. Each component will also need to emit an input event so that the parent component can pick up the change (read more here). Note that if you are passing data down through too many components, you should probably look at using Vuex however in this case it would probably still be fine.
Your components would have to look something like this to pass v-model all the way to the base component:
Parent.vue
<template>
<!-- Pass the data item below -->
<child v-model="item"></child>
</template>
<script>
import Child from './components/Child'
export default {
components: {
Child,
},
data: ()=> ({
item: ''
})
}
</script>
Child.vue
<template>
// Random HTML
// Random HTML 2
<base-select
:items="select"
value="value"
#input="e => $emit('input', e)"
>
</template>
<script>
import BaseSelect from '#/components/BaseSelect'
export default {
components: {
BaseSelect,
},
// We add the value prop below to work with v-model
props: {
value: String
},
data: ()=> ({
select: ['Select 1', 'Select 2']
}),
}
</script>
BaseSelect.vue
<template>
<v-select
v-bind="$attrs"
v-on="$listeners"
value="value"
#input="e => $emit('input', e)"
class="body-2"
solo
dense
clearable
/>
</template>
<script>
export default {
props: {
value: String
}
}
</script>
You can find a similar working example that I did here.
You need to use $emit (documentation) to passing data back to parent components. Or you can start using Vuex (state manager for Vue.js).
You also can check the live demo here.

Vue: Unknown custom element after adding component reference within component

I'm new to Vue and testing vue-draggable component. Once I add reference to vue-draggable component, I get the error "Unknown custom element: < fxm-form> - did you register the component correctly? For recursive components, make sure to provide the "name" option."
What am I missing here? Similar threads earlier does not have component referenced within a component.
import draggable from "./vue-draggable";
Vue.component('fxm-form', {
name: 'fxm-form',
props: [
"formMode"
],
components: {
draggable
},
data() {
return {
list: ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF']
}
},
mounted() {
},
methods:
{
},
template: `
<div>
<h1>Dragable Test</h1>
<draggable :list="list" class="drag-container">
<div v-for="item in list" class="drag-item">{{ item }}</div>
</draggable>
</div>
`
});
When you create a component with Vue.component(), it registers components globally.
As per official docs:
global registration must take place before the root Vue instance is created (with new Vue)
This is because either you've not initialized your component before Vue Instance.
You can register your component inside you Vue Instance.
Here is the working codesandbox example

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