I've created a wizard-type flow in Vuejs using dynamically loaded components, where the Save button is in the parent component, and all of the text fields are in the loaded components. When the Save button is clicked, I need two things to happen:
1) The next component is loaded (currently working)
2) Get data from the text field(s) in the child component and write them to my Vuex store.
Here is the code for the parent component:
<template>
<div>
<component :is="selectedComponent"></component>
<span #click="changeComponent(selectedComponent)" class="action button save">Save</span>
</div>
</template>
<script>
import auth from '#/api//auth'
import Establishment from '#/onboarding/components/OnboardingEstablishment.vue'
import Account from '#/onboarding/components/OnboardingAccount.vue'
import Vendor from '#/onboarding/components/OnboardingVendors.vue'
import Location from '#/onboarding/components/OnboardingLocation.vue'
import Menu from '#/onboarding/components/OnboardingMenu.vue'
export default {
data () {
return {
accountName: '',
selectedComponent: 'account'
}
},
components: {
establishment: Establishment,
account: Account,
vendor: Vendor,
location: Location,
appMenu: Menu
},
methods: {
changeComponent (current) {
// Mapping object to map what the next component should be
// when changing the dynamic component.
const componentFlow = {
account: 'establishment',
establishment: 'location',
location: 'vendor',
vendor: 'appMenu'
}
// Get the name of the next component.
var nextComponent = componentFlow[current]
this.selectedComponent = nextComponent
// We also need to update Vuex with the value from the text field.
},
updateState () {
// Write the data from the element to the state.
}
},
mounted () {
// Get name of logged in user.
auth.getAccountDetails()
.then(response => {
this.accountName = response.data.email
})
}
}
</script>
And as an example, here is OnboardingAccount.vue, one of the dynamically loaded components:
<template>
<div>
<h1>Hey {{ accountEmail }}</h1>
<p>Let's start at the top.</p>
<p>What do you want to name your Account?</p>
<input :value="accountName" type="text" />
<p>Note: Your account name is the top level of your management. Additional teams or establishments can be added under this account name.</p>
</div>
</template>
<script>
import auth from '#/api/auth'
import Establishment from '#/onboarding/components/OnboardingEstablishment.vue'
import Account from '#/onboarding/components/OnboardingAccount.vue'
import Vendor from '#/onboarding/components/OnboardingVendors.vue'
import Location from '#/onboarding/components/OnboardingLocation.vue'
export default {
data () {
return {
accountEmail: '',
accountName: ''
}
},
components: {
establishment: Establishment,
account: Account,
vendor: Vendor,
location: Location
},
mounted () {
// Get name of logged in user.
auth.getAccountDetails()
.then(response => {
this.accountEmail = response.data.email
})
}
}
</script>
What I need to do is somehow be able to access the value from the OnboardingAccount component when I click on the Save button in the parent Onboarding component so that I can write the date to the Vuex store, either in my changeComponent method, or elsewhere. In looking at the debugger, I don't see that value anywhere. How can I do what I need to do?
I'd keep the state for every component in the parent component and pass them down as props, then on input events #change or #blur (or other) emit an event. This is is basically standard component communication, but since you're using dynamic components this gets a bit more tricky, in passing props and events.
To pass props to dynamic components somewhat cleanly you can use v-bind, but on a whole object instead of per variable/property, like here. The properties of this object can be objects containing props for each dynamic component, also this keeps the component tag clean. Then each dynamic child component can pick out the property from the props object that is meant for it, and use that data. For the event back from the children set a generic event on the component tag, name e.g. childEvent, to which each dynamic component would report a change of its state, as in it would return the changed props, which would update the state in the parent component. Saving should then be simple as all the data is in the parent component.
Side note:
You're using dynamic components well but you can bring it to the next level if you utilize webpack's dynamic imports along with it, then you won't need to manually import each one and declare them as components. Take a look at this article.
Related
I am passing the param for breadcrumbs in array format for each page.
I get the following warning
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"items"
Breadcrumbs Component
<template>
<vs-row class="vs-breadcrumb-row" vs-type="flex" vs-justify="space-around">
<vs-col
type="flex"
vs-justify="center"
vs-align="center"
vs-lg="12"
vs-sm="12"
vs-xs="12"
code-toggler
>
<vs-card class="br-0">
<h4 class="card-title mb-0 d-flex">
<vs-row vs-justify="space-between" vs-align="center">
{{ pageTitle }}
<div class="d-flex align-items-center">
<vs-breadcrumb separator="chevron_right"
:items="breadcrumbLinks"
></vs-breadcrumb>
</div>
</vs-row>
</h4>
</vs-card>
</vs-col>
</vs-row>
</template>
<script>
export default {
name: "Breadcrumbs",
props: {
pageTitle: {
type: String
},
breadcrumbLinks: {
type: Array
}
}
};
</script>
I am getting the following error.
The layout structure is.
Dashboard - sending the array param to default layout and default layout pass it to breadcrumb
Default Layout
Breaducrumb
Dashboard Component
export default {
name: "Dashboard",
components: {
DefaultLayout
},
data: () => ({
breadcrumbLinks: [
{
title: global.vm.$t('breadcrumbs.home'),
url: ""
},
{
title: global.vm.$t('breadcrumbs.dashboard'),
active: true
}
],
})
};
The props sent to children should not be modified outside it's parent component.
Dashboard - sending the array param to default layout and default layout pass it to breadcrumb
Default Layout
Breaducrumb
Your breadcrumbLinks prop is been changed outside of Dashboard component where it was created, so, you are probably making changes on this prop on Default Layout or Breaducrumb components.
If you avoid mutating props outside its components you'll avoid this warnings.
But it's a warning, not an error, what it says is that if the parent component gets mutated, the child components, in this case Default Layout and Breaducrumb will be updated with the values previously or initially set on the parent component.
So any change you do to a prop on any child component will be lost, this may not be a problem and make your code work perfectly but it is certainly a pattern that should be avoid.
If you want to mutate props:
Send a $emit to the parent that has this props as data and performe the changes there.
Here you can read about custom events - VueDocs
Use a global state manager as Vuex
In your case you may not need any of this, please, show us the function that modifies breadcrumbLinks prop.
It should be somewhere in DefaultLayout or on Breadcrumb components, but for sure it is not on the code you posted.
I watched a video to understand how to use event bus to communicate between siblings, and in the video there was a parent with some data that was sent to the childs as a prop, then a method in one of the childs modified that prop and used an event bus to send it to the other child.
I thought, whats the point of the prop? Why cant i just use the siblings own data? And thats what i did:
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
export const bus = new Vue() //Event Bus
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<ComponenteA></ComponenteA>
<ComponenteB></ComponenteB>
</div>
</template>
<script>
import ComponenteA from './components/ComponenteA.vue'
import ComponenteB from './components/ComponenteB.vue'
export default {
name: 'App',
components:{
ComponenteA,
ComponenteB
}
}
</script>
ComponentA.vue
<template>
<div>
<h1 #click="changeTitle">Componente A</h1>
</div>
</template>
<script>
import { bus } from '../main'
export default {
name: 'ComponenteA',
data(){
return{
title: ''
}
},
methods:{
changeTitle(){
this.title = 'Title emitted from A a B'
bus.$emit('titleChanged',this.title)
}
}
}
</script>
ComponentB.vue
<template>
<div>
<h1>Componente B -> {{title}}</h1>
</div>
</template>
<script>
import { bus } from '../main'
export default {
name: 'ComponenteB',
data(){
return{
title: ''
}
},
created(){
bus.$on('titleChanged', (payload) =>{
this.title = payload
})
}
}
</script>
Is there anything wrong with my code? Is there a reason for using the parent data that im failing to see?
The reason that the data should start in the parent and get passed down to the children is because of a couple things:
The reactivity system in Vue.js is built around having your data in a single place (a "single source of truth"), then passing that data to wherever it is needed. Now, if only a single component needs the data, you'll just store that data on the component. But the data is needed in multiple components, you'll want to store it in a parent and then pass it to the children. This becomes particularly obvious if you need to start using Vuex.
If for some reason you need to change, say, the name of the data (e.g., pageTitle instead of title), it becomes far easier to trace where the data came from if it always comes from a parent. Relying on the event bus across siblings can become rather brittle as the project grows larger.
So, in your case, the title should really exist in the data of your App.vue component. Each of the children would receive it as a prop. Then, if the title was changed, $emit() an event that App.vue is listening for. That event would change the data in App.vue.
Take a look at this question for more details:
vuejs update parent data from child component
Hopefully this is an easy fix and i can just chalk it up to my inexperience with Vue
In my main template, I can use v-for to access any objects in the templates data return and I'm successfully loading a separate component in my html
However, when I try to access info from that external component outside of my component call it doesn't show.
I'm trying to create something that I can turn into a modal which would be separate from all imported components but still be able to access data from any component in my main template's modal
How can I access data from any called component in my main template?
<template>
<!--this content shows-->
<subItem-component></subItemComponent>
<!--this doesn't-->
<p v-for="subItem in subItems>{{ subItem.title }}</p>
</template>
subitemComponent
data() {
return {
subItems: [],
}
}
You cannot access the scope of a child component within the parent. Usually, you would load the required data in the parent, if you are needing it there, and then pass it along to the child component as a prop like this:
ParentComponent:
<template>
<sub-item-component sub-item="subItem"></sub-item-component>
<p v-for="subItem in subItems>{{ subItem.title }}</p>
</template>
<script>
import SubItemComponent from '...';
export default {
name: 'ParentComponent',
components: {
SubItemComponent
},
data () {
return {
subItems: []
};
}
}
</script>
SubItemComponent:
<template>
{{ subItem }}
</template>
<script>
export default {
name: 'SubItemComponent',
props: {
subItem: {
type: Array,
required: true
}
}
}
</script>
You could also use a Vuex store to persist the data in one place. With this approach you would have access to the data from each component and do not have pass the data from component to component. To use a store like Vuex, you need to define a Vuex state and at least one mutation method to add the data.
If you want to communicate from the child up to the parent, the way to go would be by using events. You would use a listener on the child component in the parent component like this:
<sub-item-component #new-sub-item="handleNewSubItem"></sub-item-component>
You need to define a method handleNewSubItem in the parent, which is reacting each time the new-sub-item event is thrown in the child component.
The event can then be thrown within the script part of the child component like this:
const subItem = { test: 123 };
this.$emit('new-sub-item', subItem);
You don't. Your options are:
The parent component needs to manage the data and pass it down to its children via props. Child components can then subsequently emit events which the parent can listen for and modify its state.
Use a state handler outside of both the components like Vuex or Apollo (if using GraphQL).
I have written some view components using single file components.
There is one master component that will have a tab control that should switch through the other components. I want all of the tabs to stay alive as you switch so you can come back. Additionally some of the tabs should consist of the same component but will display different data. The number of tabs should be dynamic as input data to the page will cause it to span more tabs.
I know that vue provides a switchable component and furthermore provides keep-alive tags to sustain the state of the component so I have code like this:
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
In the vue code I setup an array with some default tabs which I later want to add to, along with a function to return the currentTabComponent and populate the screen
import Home from './components/Home.vue';
import MessageLog from './components/MessageLog.vue';
components: {
Home,
MessageLog
},
data: function () {
return {
selectedTab: 0,
currentTab: "Home",
tabs: ['Home', 'MessageLog'],
tabComponents: new Map([["Home", Home], ["MessageLog", MessageLog]])
}
},
computed: {
currentTabComponent: function () {
this.currentTab = this.tabs[this.selectedTab];
return this.tabComponents.get(this.currentTab);
},
methods: {
testTab: function () {
this.tabs.push("nextTab");
import('./components/MessageLog.vue').then(
result =>
{
result.default.name = "nextTab";
this.tabComponents.set("nextTab", result.default);
}
);
}
}
Ideally I should be able to call testTab and have it load an INDEPENDENT component and switch to it and use it.
What happens is all this code works, but when the new tab is opened it shares state with the existing MessageLog tab, even though the component was loaded totally separately in the test tab function! I have a test button on the message log component which adds more messages and as messages are added and you swap between the two tabs with a MessageLog component the messages are all identical.
Is this some bug with the <keep-alive> option?
Or is this just a terrible design?
The goal would be that as tabs are created I can inject some ID into them (not shown here). Then we I have new data for that tab I can emit a message with its ID - it can then grab that message and do whatever is needed to show the new data.
This is my component's class:
import React, { Component } from 'react';
import {connect} from 'react-redux';
import Button from '../UI/Button/Button';
import * as actions from '../../store/actions';
class Password extends Component {
submitPassword=(e)=>{
e.preventDefault();
this.props.submitPassword(this.state.password, this.props.levelNumber);
}
render() {
<Button clicked={this.submitPassword} >
Submit password
</Button>
}
}
const mapStateToProps = state => {
return {
};
}
const mapDispatchToProps = dispatch => {
return {
submitPassword: (password,levelNumber) => dispatch(actions.submitPassword(password,levelNumber))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Password);
and this is my action:
export const submitPassword = () => {
// HERE ALL MY LOGIC !!!
return {
level:undefined,
type:actions.PASSWORD
}
}
The code working all correctly including params and logic.
I wanna that every time that finish to execute the function submitPassword A third component refresh/reload with the new props. Attention! My third component is parent, not child!
It's possibile to send a command from action to component? How can I do it? I have already tried with:
componentWillReceiveProps() {
console.log("new props");
}
in my component but he can not take the event.
normally a structure my redux store as follows
{
entities: {},
ui: {},
domain:{}
}
so for example when you execute submitPassword you execute all the logic and depending on the result you can dispatch another action to update the ui or the domain part so the componentes that are connected respond to those changes.
The UI holds information about UI changes, like if you are submiting info display a progress bar or if it has to display a dialog.
The domain part holds information related to the whole webapp, like loggin info and notifications.
You don't need always to pass new props for redux state to be accessed.
As redux is having immutable state, you'll always be getting new updated state no matter the previous one. So this will enforce your component to update props to get latest state from redux. This is done by <Provider> wrapper attached on root level.
So hence your props will be having new values whenever redux state gets updated.
The lifecycle you are looking is static getderivedstatefromprops(). This lifecycle gets executed whenever props are changed/updated.
I made visual implementation on fly that can aid you the picture. Here Redux State means Redux Store