Passing vue.js Route Params to Component - javascript

I'm having trouble getting a route param to pass directly into a component. I followed multiple sets of directions in the docs (including using the Composition API as in the following code), but I'm still getting undefined when the CourseModule.vue first renders.
Route Definition
{
path: '/module/:id',
name: 'Course Module',
props: true,
component: () => import('../views/CourseModule.vue'),
},
CourseModule.vue:
<template>
<div class="AppHome">
<CustomerItem />
<CourseModuleItem :coursemodule-id="this.CoursemoduleId"/>
</div>
</template>
<script>
import { useRoute } from 'vue-router';
import CustomerItem from '../components/customers/customer-item.vue';
import CourseModuleItem from '../components/coursemodules/coursemodule-item.vue';
export default {
setup() {
const route = useRoute();
alert(`CourseModule.vue setup: ${route.params.id}`);
return {
CoursemoduleId: route.params.id,
};
},
components: {
CustomerItem,
CourseModuleItem,
},
mounted() {
alert(`CourseModule.vue mounted: ${this.CoursemoduleId}`);
},
};
</script>
coursemodule-item.vue:
<template>
<div id="module">
<div v-if="module.data">
<h2>Course: {{module.data.ModuleName}}</h2>
</div>
<div v-else-if="module.error" class="alert alert-danger">
{{module.error}}
</div>
<Loader v-else-if="module.loading" />
</div>
</template>
<script>
import Loader from '../APILoader.vue';
export default {
props: {
CoursemoduleId: String,
},
components: {
Loader,
},
computed: {
module() {
return this.$store.getters.getModuleById(this.CoursemoduleId);
},
},
mounted() {
alert(`coursemodule-item.vue: ${this.CoursemoduleId}`);
this.$store.dispatch('setModule', this.CoursemoduleId);
},
};
</script>
The output from my alerts are as follows:
CourseModule.vue setup: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
coursemodule-item.vue: undefined
CourseModule.vue mounted: zzyClJDQ3QAKuQ2R52AC35k3Hc0yIgft
As you can see, the path parameter works fine in the top level Vue, but not it's still not getting passed into the component.

your kebab-cased :coursemodule-id props that you're passing to the CourseModuleItem component becomes a camelCased coursemoduleId props
Prop Casing (camelCase vs kebab-case)
try this
// coursemodule-item.vue
...
props: {
coursemoduleId: String,
},
...
mounted() {
alert(`coursemodule-item.vue: ${this.coursemoduleId}`);
this.$store.dispatch('setModule', this.coursemoduleId);
},

Related

Vue3: Nested Routes and Dynamic Layout component

Hi Vue enthusiasts out there,
I have been working on an multi-tenant application and stuck at dynamic layout problem.
Requirement: Load tenant specific layout.vue file from public folder and wrap <router-view> around it.
Tried few things like dynamic imports, defineAsyncComponent etc but couldn't get it working.
// router:
import store from '../store/index';
import NestedApp from '../views/NestedApp.vue';
// const layoutA = () => defineAsyncComponent(import(store.getters.pageLayout('LayoutA')));
const routes = [
{
path: '/:tenant:/:locale',
name: 'NestedApp',
component: NestedApp,
children: [
{
path: 'about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
meta: { layout: () => import(store.getters.pageLayout('LayoutA')) }
}
]
]
// NestedApp.vue:
<template>
<div class="NestedApp">
<navbar/>
<component :is="layoutWrapper">
<router-view/>
</component>
</div>
</template>
<script>
import Navbar from '../components/Navbar.vue';
export default {
name: 'NestedApp',
components: {
Navbar,
},
computed: {
layoutWrapper() {
console.info(`layout: ${this.$route.meta.layout}`);
return this.$route.meta.layout || 'div';
}
}
}
// LayoutA.vue:
<template>
<div class="LayoutA">
<span>Layout A</span>
<slot/>
</div>
</template>
I get following error in browser console:
Got a workaround to this problem.
Sending component via template string from backend API call and then creating a component out of it via defineComponent and markRaw methods.
API response:
"Layouts": {
"LayoutA": {
"name": "LayoutAbout",
"template": "<div class='LayoutA' style='background-color: darkgray'><span>Layout A</span><slot/></div>"
}
},
and then use in App.vue:
import { defineComponent, markRaw } from 'vue';
export default {
name: 'App',
methods: {
loadLayout(pageLayout) {
const layout = this.$store.getters.pageLayout(pageLayout);
this.layoutWrapper = layout ? defineComponent(markRaw({...layout})) : 'div';
}
},
created() {
this.loadLayout(this.$route.meta.layout);
},
beforeRouteUpdate(to) {
this.loadLayout(to.meta.layout);
},
}
<template>
<div class="App">
<navbar/>
<component :is="layoutWrapper">
<router-view/>
</component>
</div>
</template>

How dynamically change the component with object for props in vueJS

Do you know how to change a component dynamically with object prop
App.vue
<template>
<div id="app">
<component :is="current['test'].target.name"> </component>
<input type="button" value="click me" #click="change" />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
import Comp from "./components/Comp.vue";
export default {
name: "App",
components: {
HelloWorld,
Comp,
},
data() {
return {
current: {},
};
},
created() {
this.current["test"] = {
index: 0,
target: {
name: "Comp",
},
};
},
methods: {
change() {
const r =
this.current["test"].target.name === "HelloWorld"
? "Comp"
: "HelloWorld";
this.current["test"].target = {
name: r,
};
console.log(this.current["test"]);
},
},
};
</script>
Comp.vue
<template>
<p>Template 2</p>
</template>
HelloWorld.vue
<template>
<p>Template 1</p>
</template>
https://codesandbox.io/s/clever-water-dgbts?file=/src/components/HelloWorld.vue:0-42
The value of the object will change correctly but not the component.
Thank you
The issue here is that the property test is not defined on the object current in the data definition - you're setting the definition in the created() function. This means that Vue does not know to create the reactive getter/setter for that property.
Change your data definition to:
data() {
return {
current: {
test: {
index: 0,
target: {
name: "Comp"
}
}
}
};
}
It is because of the way Vue does its reactivity (requiring pre-defined properties) that I would recommend steering clear of accessing properties as dictionary items i.e. use:
current.test.target.name
instead of
current['test'].target.name
For more information on Vue reactivity see this page: link

Vue Recursive Component Not Rendering

I am building an application with vue.js and vue-cli where you can configure an order. The user should be able to choose whether they want to configure another order after each order or if they are done. To achieve this, I want to reuse my order component recursively:
Order.vue:
<template>
<div id="order">
<other> // just to show that I used other components here and how
<div>
<button class="CustomButton" v-on:click="finalizeOrder">
Finalize Order
</button>
<button class="CustomButton" v-on:click="configureAdditionalOrder">
Configure Additional Order
</button>
</div>
<order v-if="additionalOrder" #passOrderObject="catchOrderObjectFromRecursiveChild" #finalizeOrder="passFinalizeOrder" />
</div>
</template>
<script>
import Other from '#/components/Other.vue'
export default {
components: {
Other
},
data () {
return {
additionalOrder: false
}
},
methods: {
configureAdditionalOrder () {
this.buildOrderObject()
this.additionalOrder = true
},
catchOrderObjectFromRecursiveChild (order) {
this.$emit('passOrderObject', order)
},
passFinalizeOrder () {
this.$emit('finalizeOrder')
},
finalizeOrder () {
this.buildOrderObject()
this.$emit('finalizeOrder')
},
buildOrderObject () {
var order = {
name: "abc",
price: "1000"
}
this.$emit('passOrderObject', order)
}
}
</script>
<style>
</style>
App.vue:
<template>
<div id="app">
<h1>Ordering System</h1>
<order #passOrderObject="catchOrderObject" #finalizeOrder="finalizeOrder" />
</div>
</template>
<script>
import Vue from 'vue'
import Order from '#/components/Order.vue'
export default {
el: '#app',
components: {
Order
},
data () {
return {
finalization: false,
orderObjects: []
}
},
methods: {
finalizeOrder () {
this.finalization = true
},
catchOrderObject (order) {
this.orderObjects.push(order)
}
</script>
As you can see, I used a boolean variable in my component that should show the same component an additional time if the button "Configure Additional Order" is clicked. I use custom events to pass data to the parent component (App.vue), and the order component can handle these events from their recursive children by just passing them further.
The app itself works, but clicking the button to configure an additional order does nothing except emitting the custom event. What did I do wrong here?
For recursive component you should provide a name to that component :
<script>
import Other from '#/components/Other.vue'
export default {
name:'order',// this is the component name
components: {
Other
},
data () {
return {
additionalOrder: false
}
},
...

vueJS - Using 'this' in a callback function

EDIT --- SOLVED
It turns out that isn't really a problem, Vue will auto-bind for you so there's no need to bind manually.
END EDIT ---
I'm trying to pass a method to a callback(or event) to a child component.
Everything works great, except that the function executes in the wrong context.
In react, I would bind the functions in the constructor, I'm not sure what's the solution is in Vue.
Example
<template>
<div id="app">
<Header/>
<Tasks
:todos="todos"
#onMarkAsDone="markAsDone"
>
</Tasks>
</div>
</template>
<script>
import Header from './components/Header.vue';
import Tasks from './components/Tasks.vue';
export default {
name: 'my-component',
data() {
return {
name: 'Tasks',
todos: [{
id:0,
text:'Complete this by lunch',
isDone: false
}]
}
},
methods: {
markAsDone(id) {
console.log(this); // refers to the child component
// can't access this.todos
}
},
components: {
Tasks,
Header
}
}
</script>
Here's the solution to it, turns out you can use the 'created' life cycle hook, this is similar to react when binding in a constructor
<template>
<div id="app">
<Header/>
<Tasks
:todos="todos"
#onMarkAsDone="markAsDone"
>
</Tasks>
</div>
</template>
<script>
import Header from './components/Header.vue';
import Tasks from './components/Tasks.vue';
export default {
name: 'my-component',
data() {
return {
name: 'Tasks',
todos: [{
id:0,
text:'Complete this by lunch',
isDone: false
}]
}
},
methods: {
markAsDone(id) {
console.log(this.todos); // can now access the correct 'this'
}
},
created() {
this.markAsDone = this.markAsDone.bind(this);
},
components: {
Tasks,
Header
}
}
</script>
Sub component code
<template>
<ul>
<li
:class="{isDone:todo.isDone}"
:key="todo.id"
v-for="todo in todos">
<input type='checkbox' #change="markAsDone(todo.id)"/>
{{todo.text}}
</li>
</ul>
</template>
<script>
export default {
name: 'Tasks',
props: ['todos'],
methods: {
markAsDone(id) {
this.$emit('onMarkAsDone', id);
}
}
}
</script>
You can return function in a markAsDone method, like this:
markAsDone() {
return id => {
console.log(this.todos);
}
},
and then when passing method as a prop call it:
:onMarkAsDone="markAsDone()"
Then you can call the prop method inside your Child-Component.
That should give you requested functionality without using bind in created() hook.

Trouble passing form fields between components at higher level form

I'm getting started with Vue, I need to create a form of tiered select fields. That is the selected option for A, uses that to call the API to get the options for B, which determines options for C.
I'm still pretty new to frontend frameworks so this might be a terrible design. However not every inclusion of A (SelectState.vue) in a view requires all the children so making them modular was my first thought.
Currently I have a top level component that displays the select options:
SelectState.vue
<template>
<div id="select-state">
<span>{{ label }}</span>
<select v-model="selectedState">
<option v-for="state in states" :key="state">
{{ state }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'select-state',
data: function () {
return {
selectedState: '',
states: ['TX']
}
},
props: ['label']
// this.states = axios.get('xxx')
}
</script>
Index.vue
<template>
<div id="form">
<v-select-state label="State"></v-select-state>
<v-select-zip label="Zip"></v-select-zip>
</div>
</template>
<script>
import SelectState from './SelectState.vue'
import SelectZip from './SelectZip.vue'
export default {
name: 'Index',
components: {
'v-select-state': SelectState,
'v-select-Zip': SelectZip
}
}
</script>
Then I have a SelectZip.vue that is identical to SelectState.vue except that it has a parameter for its axios.get('XXX', params = {'state': ???}). But I'm stuck on how to "pass" that necessary parameter.
Thanks in advance!
edit: In conjunction with #dziraf's answer my working although verbose SelectedZip.vue is as follows:
<template>
<div id="select_zip">
<span>{{ label }}</span>
<select v-model="selected_zip">
<option v-for="zip in zips" :key="zip">
{{ zip }}
</option>
</select>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'select_zip',
data: function () {
return {
zips: []
}
},
props: ['label'],
computed: {
selected_zip: {
get () { return this.$store.state.formModule.zip },
set (value) { this.$store.commit('formModule/setZips', value) }
},
selected_state: {
get () { return this.$store.state.formModule.state }
}
},
methods: {
getValidZips (state) {
axios.post('/api/v1/get_valid_zips', {
params:{'state': state }})
.then(response => {
this.zips = response.data
})
.catch(error => {
console.log(error)
})
}
},
watch: {
selected_state (value) {
this.getValidZips(value)
}
}
}
</script>
You can pass it by adding 'state' props to your select components from your main form component, but I think it isn't a good long-term solution.
Instead, consider using Vuex. An example configuration would look like this:
#/store/modules/form.js
const Form = {
namespaced: true,
state: {
state: '',
zip: ''
},
getters: {},
mutations: {
setState (state, payload) {
state.state = payload
},
setZip (state, payload) {
state.zip = payload
}
},
actions: {}
}
export default Form
#/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Form from './modules/form'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
formModule: Form,
}
})
export default store
#/main.js
// your impots
import store from './store/index'
// your configs
new Vue({
el: '#app',
router,
store, // add store to your main Vue instance so it's accessible with this.$store
axios,
components: { App },
template: '<App/>'
});
This would be your SelectState.vue:
<template>
<div id="select-state">
<span>{{ label }}</span>
<select v-model="selectedState">
<option v-for="state in states" :key="state">
{{ state }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'select-state',
data: function () {
return {
states: ['TX']
}
},
computed: {
selectedState: {
get() { return this.$store.state.formModule.state },
set(value) { this.$store.commit('formModule/setState', value) }
}
},
props: ['label']
}
</script>
Your SelectZip.vue would be the same, except you would instead use your store's zip as your v-model.
Your store variables are accessible across your app and you can access them with computed properties in your components.

Categories

Resources