Vue.js dynamic components - passing data - vue.js

I'm new to Vue.js. I followed the tutorial here - https://coligo.io/dynamic-components-in-vuejs/ - on dynamic components, to give me a dynamic layout I liked, for listing products and allowing the user to switch to an edit view when they click on one of the products in the table. So, I have a 'list-products' component and an 'edit-product' component, and which one is displayed is dependent on the state of 'currentView' in the main Vue instance.
<div id="content">
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</div>
The switching is all working fine when currentView is changed. What I haven't got figured out is how best to pass the product information to the edit component such that it ends up as data. I suppose the list and edit components are two sibling components of the main Vue instance, instantiated at the same time. What I need to do is when I click on a row in listing table, have the product object used for building that row made available to the edit component. I'm not sure how I do that (at least, in a proper Vue way). When the displayed component is switched (via the change in 'currentView'), is some event called for the newly (re)displayed component? If so, I could presumably call some function?
LATER: I have determined that the 'activated' hook is called when I switch to the edit-product component, which I imagine I should be able to use somehow. Now to figure that out.

You could use Vuex for that. Vuex is a Flux inspired state management library for Vue.
Your application basically has two different states: (1) no product selected (list-products component), and (2) any product selected (edit-product). When this is modeled with Vuex, the idea is to keep the currently selected product in a so-called store and let the components figure out their internal state depending on the store state. Here's an example:
Create a store to keep the application state:
const store = new Vuex.Store({
state: {
selectedProduct: null
},
getters: {
selectedProduct: state => state.selectedProduct
},
mutations: {
selectProduct: (state, data) => state.selectedProduct = data
}
});
Handle product selection in your list-products component:
methods: {
selectProduct(product) {
this.$store.commit('selectProduct', product);
}
}
Display the current product in edit-product:
Vue.component('edit-product', {
store,
template: '<div>{{selectedProduct.name}}</div>',
computed: Vuex.mapGetters(['selectedProduct'])
});
And finally switch the components depending on the state:
new Vue({
el: '#app',
store,
computed: Object.assign(Vuex.mapGetters(['selectedProduct']), {
currentView() {
return this.selectedProduct ? 'edit-product' : 'list-products'
}
})
});
Here's a basic working JSFiddle.

Related

Pass the value of a variable from child component to two parent components, vuejs? nuxt

How to store the state value of a variable?
For example, I have three components: home.vue and map.vue, as well as a common sidebar.vue component, which is used inside home.vue and map.vue.
I pass some data from sidebar.vue (a variable storing sidebar width state). And this data should change dynamically in both components. That is, when switching from home.vue to map.vue, the state of the variable for changing the width of sidebar.vue should not change.
You can store the width of the sidebar in Vuex store.
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
Create the store:
const store = new Vuex.Store({
state: {
width: 50
},
mutations: {
setWidth (state, value) {
state.width = value
}
}
})
"Inject" the store into all child components from the root component with the store option (enabled by Vue.use(Vuex)):
const app = new Vue({
el: '#app',
// provide the store using the "store" option.
// this will inject the store instance to all child components.
store,
...
})
By providing the store option to the root instance, the store will be injected into all child components of the root and will be available on them as this.$store. When the width is changed update it by calling mutation method in the methods of your components:
this.$store.commit('setWidth', value)
To read the value of the width from the store in any method of your vue-components use this:
this.$store.state.width

How to share variable with Nuxt.js from layout to pages?

I actually work on a project that consists of display data from Jsons on a Nuxt.js website using Vuetify. I have created a selector in my layout to choose which Json the user wants to display. I need to access this variable from all the different pages of my project.
Here is what my default.vue looks like :
<template>
<v-overflow-btn
:items="json_list"
label="Select an Json to display"
v-model="selected_json"
editable
mandatory
item-value="text"
></v-overflow-btn>
</template>
<script>
export default {
data() {
return {
selected_json: undefined,
json_list: [
{text: "first.json"},
{text: "second.json"},
],
}
}
}
</script>
The variable I would like to access from all my different pages is selected_json.
I see many things on the internet such as Vuex or a solution that consist to pass the variable with the URL. But I'm kind of newby in web programming (started Vue/Nuxt one week ago) and I don't really understand how to apply this in my project. So if there is a more easy way to do it or a good explaination, I'm interested!
Thanks in advance for your help :)
Using Vuex we can easily achieve what you want.
First of all create a file index.js in folder store (if you don't have a store folder then create it in the same directory where your pages, plugins, layouts etc folders are). Then paste this piece of code in index.js
//store/index.js
export const state = () => ({
selected_json: null
})
By doing this we set Vuex up. More precisely just state part of Vuex where if you don't know we store data accessible across your project.
Now we have to assign data from your default.vue to Vuex. We can achieve this by creating a mutation function through which we change state in Vuex. Add this to your index.js
//store/index.js
export const mutations = {
setSelectedJson(state, selectedJson) {
state.selected_json = selectedJson
}
}
Here function setSelectedJson takes two params, state which is automatically passed in by Nuxt.js and it includes all our Vuex state data. The second parameter selected_json we pass in ourselves.
Now in your default.vue we need to add a watcher for selected_json so we can update our Vuex when selected_json gets updated.
//layouts/default.vue
export default {
watch: {
selected_json(newValue) {
this.$store.commit("setSelectedJson", newValue)
}
}
}
We are almost done.
The last thing we need to do is to make a getter which is used to retrieve values from Vuex. A getter like this will do its job.
//store/index.js
export const getters = {
getSelectedJson(state) {
return state.selected_json
}
}
That's it.
Now you can access selected_json on any page you want by simply getting it from Vuex with this line of code.
this.$store.getters["getSelectedJson"]

VUE.JS - Using the vNode argument to add unknown quantity of items to a dynamic navigation

I'm currently trying to create a component which manages several linked dropdowns and elements on a page. In addition, this element supplies a rather fancy navigation element, containing anchor links which automatically scroll to the desired element in this component.
The problem is that the actual contents of the component are completely dynamic and partially determined by the content manager in the CMS. There are several sub components that are always present, but apart from that the content manager can add any number of sections, (using various named and an unnamed ) and each of these sections should be added to the navigation of the component.
I see 3 options:
For every component added, it's title and unique id is added to a property array on the parent component. This, however, is a bit prone to errors. (Unfortunately I will have no control over the eventual backend implementation, so I'm trying to create a foolproof system to avoid to much wasted time.)
Due to other components, I'm already using Vuex to manage most of the data in the app. I figured I use a simple directive, to be added on each item in the parent component. This directive is responsible for adding that element to the Vuex store. The parent component simply reads the contents of the store and generates the navigation based on this.
The problem here is that, as far as I can tell, I have to use the vNode argument in the bind hook of my directive to access the Vuex store. This seems... hacky. Is there anything wrong with this approach?
In the mounted hook of my parent component, I traverse the DOM, searching for elements with a particular data-property, and add a link to each of these elements to my navigation. Seems prone to failure and fragile.
What is the preferred approach for this situation?
As a follow up question - What is the correct use case for the vNode argument in a vue directive? The documentation seems rather sparse on this subject.
I would steer away from using a directive in this case. In Vue 2, the primary use case for directives is for low level DOM manipulation.
Note that in Vue 2.0, the primary form of code reuse and abstraction
is components - however there may be cases where you just need some
low-level DOM access on plain elements, and this is where custom
directives would still be useful.
Instead I would suggest a mixin approach, where your mixin essentially registers your components that should be included in navigation with Vuex.
Consider the following code.
const NavMixin = {
computed:{
navElement(){
return this.$el
},
title(){
return this.$vnode.tag.split("-").pop()
}
},
mounted(){
this.$store.commit('addLink', {
element: this.navElement,
title: this.title
})
}
}
This mixin defines a couple of computed values that determine the element that should be used for navigation and the title of the component. Obviously the title is a placeholder and you should modify it to suit your needs. The mounted hook registers the component with Vue. Should a component need a custom title or navElement, mixed in computed properties are overridden by the component's definition.
Next I define my components and use the mixin.
Vue.component("child1",{
mixins:[NavMixin],
template:`<h1>I am child1</h1>`
})
Vue.component("child2",{
mixins:[NavMixin],
template:`<h1>I am child2</h1>`
})
Vue.component("child3",{
template:`<h1>I am child3</h1>`
})
Note that here I am not adding the mixin to the third component, because I could conceive of a situation where you may not want all components included in navigation.
Here is a quick example of usage.
console.clear()
const store = new Vuex.Store({
state: {
links: []
},
mutations: {
addLink (state, link) {
state.links.push(link)
}
}
})
const NavMixin = {
computed:{
navElement(){
return this.$el
},
title(){
return this.$vnode.tag.split("-").pop()
}
},
mounted(){
this.$store.commit('addLink', {
element: this.navElement,
title: this.title
})
}
}
Vue.component("child1",{
mixins:[NavMixin],
template:`<h1>I am child1</h1>`,
})
Vue.component("child2",{
mixins:[NavMixin],
template:`<h1>I am child2</h1>`
})
Vue.component("child3",{
template:`<h1>I am child3</h1>`
})
Vue.component("container",{
template:`
<div>
<button v-for="link in $store.state.links" #click="scroll(link)">{{link.title}}</button>
<slot></slot>
</div>
`,
methods:{
scroll(link){
document.querySelector("body").scrollTop = link.element.offsetTop
}
},
})
new Vue({
el:"#app",
store
})
h1{
height:300px
}
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.js"></script>
<div id="app">
<container>
<child1></child1>
<child3></child3>
<child2></child2>
</container>
</div>
This solution is pretty robust. You do not need to parse anything. You have control over which components are added to the navigation. You handle potentially nested components. You said you don't know which types of components will be added, but you should have control over the definition of the components that will be used, which means its relatively simple to include the mixin.

Passing in two-way binding prop to a slot

I'm somewhat new to Vue, and I'm having particular difficulty in passing in formData to my individual child nodes. Ideally each child node simply updates the parent formData object allowing me to submit the form data later on as a whole.
I've setup a JSFiddle to illustrate: https://jsfiddle.net/3nm1mrLo/
My current thinking is that I should v-bind:field="formData.name" but that throws an error. It seems as though formData doesn't exist in the slot based HTML.
Any pointers would be gratefully received. Thanks!
As you rightly said, you need to use v-bind:field="formData.name" or :field="formData.name".
It is not working because you have defined the main app template directly in your html, and used "content distribution" to include <example-form> and <example-input>.
As defined in the docs, this content-distribution (or "transclusion" if you are familiar with Angular 1.x) works fine, but the scope belongs to the main app (the root instance, because the template belongs to it).
Ref: https://v2.vuejs.org/v2/guide/components.html#Compilation-Scope
Quote:
A simple rule of thumb for component scope is:
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in child scope.
If you are curious, try changing your main app (root instance) as follows:
new Vue({
el: '*[my-app]',
data: function() {
return {
formData: { name: 'abc', location: 'xyz'}
};
}
});
Now you will see that formData is not undefined anymore.
But a more proper method is to include <example-input> as part of the template of example-form component as follows:
Vue.component('example-form', {
template: `
<div class="my-example-form">
<form><pre>{{formData}}</pre><slot></slot></form>
<example-input :field="formData.name"></example-input>
<example-input :field="formData.location"></example-input>
</div>
`,
data: function() {
return {
formData: { name: '', location: ''}
};
}
});
Now it will bind to the right formData as you would expect.
But this will still not work for you because props is a one-way binding only. The child component (example-input) will get the value, but will not pass the data changes back to parent component (example-form)
For child to pass data back to parent, the right way is to use $emit as explained in this question: Updating parent data via child component?
If you want to have an <example-input> component to work as form elements, here is a related answer which works like what you expect: https://stackoverflow.com/a/40337942/654825 - there is a working jsFiddle also.

VueJS access child component's data from parent

I'm using the vue-cli scaffold for webpack
My Vue component structure/heirarchy currently looks like the following:
App
PDF Template
Background
Dynamic Template Image
Static Template Image
Markdown
At the app level, I want a vuejs component method that can aggregate all of the child component's data into a single JSON object that can be sent off to the server.
Is there a way to access child component's data? Specifically, multiple layers deep?
If not, what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values? I'm trying to avoid hard dependencies between components, so as of right now, the only thing passed using component attributes are initialization values.
UPDATE:
Solid answers. Resources I found helpful after reviewing both answers:
Vuex and when to use it
Vuex alternative solution for smaller apps
In my child component, there are no buttons to emit changed data. It's a form with somewhat 5~10 inputs. the data will be submitted once you click the process button in another component. so, I can't emit every property when it's changing.
So, what I did,
In my parent component, I can access child's data from "ref"
e.g
<markdown ref="markdowndetails"></markdown>
<app-button #submit="process"></app-button>
// js
methods:{
process: function(){
// items is defined object inside data()
var markdowns = this.$refs.markdowndetails.items
}
}
Note: If you do this all over the application I suggest move to vuex instead.
For this kind of structure It's good to have some kind of Store.
VueJS provide solution for that, and It's called Vuex.If you are not ready to go with Vuex, you can create your own simple store.
Let's try with this
MarkdownStore.js
export default {
data: {
items: []
},
// Methods that you need, for e.g fetching data from server etc.
fetchData() {
// fetch logic
}
}
And now you can use those data everywhere, with importing this Store file
HomeView.vue
import MarkdownStore from '../stores/MarkdownStore'
export default {
data() {
sharedItems: MarkdownStore.data
},
created() {
MarkdownStore.fetchData()
}
}
So that's the basic flow that you could use, If you dont' want to go with Vuex.
what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values?
The flow of props is one way down, a child should never modify its props directly.
For a complex application, vuex is the solution, but for a simple case vuex is an overkill. Just like what #Belmin said, you can even use a plain JavaScript object for that, thanks to the reactivity system.
Another solution is using events. Vue has already implemented the EventEmitter interface, a child can use this.$emit('eventName', data) to communicate with its parent.
The parent will listen on the event like this: (#update is the shorthand of v-on:update)
<child :value="value" #update="onChildUpdate" />
and update the data in the event handler:
methods: {
onChildUpdate (newValue) {
this.value = newValue
}
}
Here is a simple example of custom events in Vue:
http://codepen.io/CodinCat/pen/ZBELjm?editors=1010
This is just parent-child communication, if a component needs to talk to its siblings, then you will need a global event bus, in Vue.js, you can just use an empty Vue instance:
const bus = new Vue()
// In component A
bus.$on('somethingUpdated', data => { ... })
// In component B
bus.$emit('somethingUpdated', newData)
you can meke ref to child component and use it as this
this.$refs.refComponentName.$data
parent-component
<template>
<section>
<childComponent ref="nameOfRef" />
</section>
</template>
methods: {
save() {
let Data = this.$refs.nameOfRef.$data;
}
},
In my case I have a registration form that I've broken down into components.
As suggested above I used $refs, In my parent I have for example:
In Template:
<Personal ref="personal" />
Script - Parent Component
export default {
components: {
Personal,
Employment
},
data() {
return {
personal: null,
education: null
}
},
mounted: function(){
this.personal = this.$refs.personal.model
this.education = this.$refs.education.model
}
}
This works well as the data is reactive.